Merge branch 'master' into master
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"flutterSdkVersion": "3.16.0",
|
"flutterSdkVersion": "3.19.1",
|
||||||
"flavors": {}
|
"flavors": {}
|
||||||
}
|
}
|
||||||
4
.github/workflows/pr-lint.yml
vendored
@ -4,13 +4,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: '3.16.0'
|
FLUTTER_VERSION: '3.19.5'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
|||||||
2
.github/workflows/spotube-publish-binary.yml
vendored
@ -66,7 +66,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Release to AUR
|
- name: Release to AUR
|
||||||
if: ${{ !inputs.dry_run }}
|
if: ${{ !inputs.dry_run }}
|
||||||
uses: KSXGitHub/github-actions-deploy-aur@v2.7.0
|
uses: KSXGitHub/github-actions-deploy-aur@v2.7.1
|
||||||
with:
|
with:
|
||||||
pkgname: spotube-bin
|
pkgname: spotube-bin
|
||||||
pkgbuild: aur-struct/PKGBUILD
|
pkgbuild: aur-struct/PKGBUILD
|
||||||
|
|||||||
21
.github/workflows/spotube-release-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: Version to release (x.x.x)
|
description: Version to release (x.x.x)
|
||||||
default: 3.4.1
|
default: 3.6.0
|
||||||
required: true
|
required: true
|
||||||
channel:
|
channel:
|
||||||
type: choice
|
type: choice
|
||||||
@ -26,7 +26,7 @@ on:
|
|||||||
default: true
|
default: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: '3.16.3'
|
FLUTTER_VERSION: '3.19.1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
windows:
|
windows:
|
||||||
@ -181,6 +181,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ inputs.channel == 'stable' }}
|
||||||
with:
|
with:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
name: Spotube-Release-Binaries
|
name: Spotube-Release-Binaries
|
||||||
@ -188,6 +189,16 @@ jobs:
|
|||||||
dist/Spotube-linux-x86_64.deb
|
dist/Spotube-linux-x86_64.deb
|
||||||
dist/Spotube-linux-x86_64.rpm
|
dist/Spotube-linux-x86_64.rpm
|
||||||
dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
|
dist/spotube-linux-${{ env.BUILD_VERSION }}-x86_64.tar.xz
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ inputs.channel == 'nightly' }}
|
||||||
|
with:
|
||||||
|
if-no-files-found: error
|
||||||
|
name: Spotube-Release-Binaries
|
||||||
|
path: |
|
||||||
|
dist/Spotube-linux-x86_64.deb
|
||||||
|
dist/Spotube-linux-x86_64.rpm
|
||||||
|
dist/spotube-linux-nightly-x86_64.tar.xz
|
||||||
|
|
||||||
- name: Debug With SSH When fails
|
- name: Debug With SSH When fails
|
||||||
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
if: ${{ failure() && inputs.debug && inputs.channel == 'nightly' }}
|
||||||
@ -273,7 +284,7 @@ jobs:
|
|||||||
|
|
||||||
macos:
|
macos:
|
||||||
|
|
||||||
runs-on: macos-12
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: subosito/flutter-action@v2.12.0
|
- uses: subosito/flutter-action@v2.12.0
|
||||||
@ -316,7 +327,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Package Macos App
|
- name: Package Macos App
|
||||||
run: |
|
run: |
|
||||||
python3 -m pip install setuptools
|
brew install python-setuptools
|
||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
mkdir -p build/${{ env.BUILD_VERSION }}
|
mkdir -p build/${{ env.BUILD_VERSION }}
|
||||||
appdmg appdmg.json build/Spotube-macos-universal.dmg
|
appdmg appdmg.json build/Spotube-macos-universal.dmg
|
||||||
@ -338,7 +349,7 @@ jobs:
|
|||||||
limit-access-to-actor: true
|
limit-access-to-actor: true
|
||||||
|
|
||||||
iOS:
|
iOS:
|
||||||
runs-on: macos-latest
|
runs-on: macos-14
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: subosito/flutter-action@v2.10.0
|
- uses: subosito/flutter-action@v2.10.0
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
@ -2,11 +2,19 @@
|
|||||||
"cmake.configureOnOpen": false,
|
"cmake.configureOnOpen": false,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"acousticness",
|
"acousticness",
|
||||||
|
"ambiguate",
|
||||||
|
"Amoled",
|
||||||
|
"Buildless",
|
||||||
"danceability",
|
"danceability",
|
||||||
|
"fuzzywuzzy",
|
||||||
|
"gapless",
|
||||||
"instrumentalness",
|
"instrumentalness",
|
||||||
"Mpris",
|
"Mpris",
|
||||||
|
"RGBO",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
"Scrobblenaut",
|
"Scrobblenaut",
|
||||||
|
"skeletonizer",
|
||||||
|
"songlink",
|
||||||
"speechiness",
|
"speechiness",
|
||||||
"Spotube",
|
"Spotube",
|
||||||
"winget"
|
"winget"
|
||||||
|
|||||||
170
.vscode/snippets.code-snippets
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"PaginatedState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedState",
|
||||||
|
"description": "Generate a PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class ${1:Model}State extends PaginatedState<${2:Model}> {",
|
||||||
|
" ${1:Model}State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" ${1:Model}State copyWith({",
|
||||||
|
" List<${2:Model}>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return ${1:Model}State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PaginatedAsyncNotifier": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedAsyncNotifier",
|
||||||
|
"description": "Generate a PaginatedAsyncNotifier",
|
||||||
|
"body": [
|
||||||
|
"class ${1:NotifierName}Notifier extends PaginatedAsyncNotifier<${3:Item}, ${2:Model}State> {",
|
||||||
|
" ${1:NotifierName}Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(int offset, int limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build() async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PaginaitedNotifierWithState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "paginatedNotifierWithState",
|
||||||
|
"description": "Generate a PaginatedNotifier with PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class $1State extends PaginatedState<$2> {",
|
||||||
|
" $1State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" $1State copyWith({",
|
||||||
|
" List<$2>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return $1State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"class $1Notifier",
|
||||||
|
" extends PaginatedAsyncNotifier<$2, $1State> {",
|
||||||
|
" $1Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(int offset, int limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build() async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProvider<$1Notifier, $1State>(",
|
||||||
|
" ()=> $1Notifier(),",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FamilyPaginatedAsyncNotifier": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "familyPaginatedAsyncNotifier",
|
||||||
|
"description": "Generate a FamilyPaginatedAsyncNotifier",
|
||||||
|
"body": [
|
||||||
|
"class ${1:NotifierName}Notifier extends FamilyPaginatedAsyncNotifier<${3:Item}, ${2:Model}State, {$4:Arg}> {",
|
||||||
|
" ${1:NotifierName}Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(arg, offset, limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build(arg) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FamilyPaginaitedNotifierWithState": {
|
||||||
|
"scope": "dart",
|
||||||
|
"prefix": "familyPaginatedNotifierWithState",
|
||||||
|
"description": "Generate a FamilyPaginatedAsyncNotifier with PaginatedState",
|
||||||
|
"body": [
|
||||||
|
"class $1State extends PaginatedState<$2> {",
|
||||||
|
" $1State({",
|
||||||
|
" required super.items,",
|
||||||
|
" required super.offset,",
|
||||||
|
" required super.limit,",
|
||||||
|
" required super.hasMore,",
|
||||||
|
" });",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" $1State copyWith({",
|
||||||
|
" List<$2>? items,",
|
||||||
|
" int? offset,",
|
||||||
|
" int? limit,",
|
||||||
|
" bool? hasMore,",
|
||||||
|
" }) {",
|
||||||
|
" return $1State(",
|
||||||
|
" items: items ?? this.items,",
|
||||||
|
" offset: offset ?? this.offset,",
|
||||||
|
" limit: limit ?? this.limit,",
|
||||||
|
" hasMore: hasMore ?? this.hasMore,",
|
||||||
|
" );",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"class $1Notifier",
|
||||||
|
" extends FamilyPaginatedAsyncNotifier<$2, $1State, $3> {",
|
||||||
|
" $1Notifier() : super();",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" fetch(arg, offset, limit) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
" ",
|
||||||
|
" @override",
|
||||||
|
" build(arg) async {",
|
||||||
|
" throw UnimplementedError();",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
" ",
|
||||||
|
"final ${1/(.*)/${1:/camelcase}/}Provider = AsyncNotifierProviderFamily<$1Notifier, $1State, $3>(",
|
||||||
|
" ()=> $1Notifier(),",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
53
CHANGELOG.md
@ -2,6 +2,59 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [3.6.0-0](https://github.com/krtirtho/spotube/compare/v3.5.0...v3.6.0-0) (2024-04-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Spotify homepage personalized recommendations ([#1402](https://github.com/krtirtho/spotube/issues/1402)) ([9e25c74](https://github.com/krtirtho/spotube/commit/9e25c742d4e43e4e10d2b48afb8e6d90288ffa11))
|
||||||
|
* add user profile page ([39e97ee](https://github.com/krtirtho/spotube/commit/39e97eef34d87348a264843e145f31f82832d12e))
|
||||||
|
* **android:** Filter Device To Force High Frame Rate ([#880](https://github.com/krtirtho/spotube/issues/880)) ([6e41b10](https://github.com/krtirtho/spotube/commit/6e41b106fa989adee393d3ce2535e75446ad3eea))
|
||||||
|
* improved caching based on riverpod ([#1343](https://github.com/krtirtho/spotube/issues/1343)) ([6673e5a](https://github.com/krtirtho/spotube/commit/6673e5a8a86b9667cf9dbff9bb7c40ea6b7de771))
|
||||||
|
* LAN connect a.k.a control remote Spotube playback and local output device selection ([#1355](https://github.com/krtirtho/spotube/issues/1355)) ([68374ef](https://github.com/krtirtho/spotube/commit/68374efd3ec556f31b937e5b96920787b54eec78))
|
||||||
|
* **lyrics:** add LRCLIB lyrics provider as fallback ([5afe823](https://github.com/krtirtho/spotube/commit/5afe823abdb198340b55d138d8173d886a811632))
|
||||||
|
* search history support [#1236](https://github.com/krtirtho/spotube/issues/1236) ([82b1cfa](https://github.com/krtirtho/spotube/commit/82b1cfa0d775e3958c666280943a893c9113d468))
|
||||||
|
* **translations:** Add Czech translation ([#1401](https://github.com/krtirtho/spotube/issues/1401)) ([5a6b800](https://github.com/krtirtho/spotube/commit/5a6b80091259359bc38c4b91cd8cb496c4270fa4))
|
||||||
|
* **translations:** add Thai Language ([#1319](https://github.com/krtirtho/spotube/issues/1319)) ([b70f250](https://github.com/krtirtho/spotube/commit/b70f250e8d5137fd990787ec9e3d058126cf14f3)), closes [#1310](https://github.com/krtirtho/spotube/issues/1310) [#1311](https://github.com/krtirtho/spotube/issues/1311)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* instance of Artist bug [#1362](https://github.com/krtirtho/spotube/issues/1362) ([c8dd802](https://github.com/krtirtho/spotube/commit/c8dd8025ec96bd78ed77cae35f1429aa48c16fde))
|
||||||
|
* **playback:** sponsor block skips and stutters in same position ([0d080b7](https://github.com/krtirtho/spotube/commit/0d080b77b72529c0be5ebc27ace1c52307511f73))
|
||||||
|
|
||||||
|
## [3.5.0](https://github.com/krtirtho/spotube/compare/v3.4.1...v3.5.0) (2024-03-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add endless playback support [#285](https://github.com/krtirtho/spotube/issues/285) ([9dfd49c](https://github.com/krtirtho/spotube/commit/9dfd49ca04f0e915e333e205b17ac70456873f6e))
|
||||||
|
* add getting started page ([96a2a1f](https://github.com/krtirtho/spotube/commit/96a2a1f5a622cb3c580041417d5023e37fa69716))
|
||||||
|
* Add iOS background play support ([#1166](https://github.com/krtirtho/spotube/issues/1166)) ([095587e](https://github.com/krtirtho/spotube/commit/095587ee84f7d867c69fcf4b09ed608d63478e1e))
|
||||||
|
* add songlink based track matching for youtube and open song link button ([9095a8c](https://github.com/krtirtho/spotube/commit/9095a8c8f849e42daabb7efcc20085cfb863c974))
|
||||||
|
* **playlist:** show confirmation before deleting user playlist [#1222](https://github.com/krtirtho/spotube/issues/1222) ([9f92440](https://github.com/krtirtho/spotube/commit/9f9244062a39759aa0ce28d2d5f7c8fa53d73003))
|
||||||
|
* Sort by Duration ([#1238](https://github.com/krtirtho/spotube/issues/1238)) ([6f8271f](https://github.com/krtirtho/spotube/commit/6f8271f5e9394cb4053e41dd222aa2844c34d609))
|
||||||
|
* start radio support ([4defeef](https://github.com/krtirtho/spotube/commit/4defeefe7e5947aa00a2afb2a06577ec141cdc52))
|
||||||
|
* **translations:** add Korean translation ([#1275](https://github.com/krtirtho/spotube/issues/1275)) ([fdea930](https://github.com/krtirtho/spotube/commit/fdea9307bbfb8f3f62cfb795bfb3ca58c38c33d9))
|
||||||
|
* **translations:** Added Vietnamese ([#1135](https://github.com/krtirtho/spotube/issues/1135)) ([019ba86](https://github.com/krtirtho/spotube/commit/019ba865e20a8b54ea3490c01e47158eaf3a4c8d))
|
||||||
|
* **windows:** Install Visual C++ 2015-2022 Redistributable if missing when installing ([ba69496](https://github.com/krtirtho/spotube/commit/ba69496dcc9a1b7f6ea4e104e71764a854d27f1f))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* album images are small in certain places ([ca76a39](https://github.com/krtirtho/spotube/commit/ca76a39910b1a5af91aa7882a0d33c9d71db58a2))
|
||||||
|
* album, artist page not loading [#1282](https://github.com/krtirtho/spotube/issues/1282) ([a9a1d4c](https://github.com/krtirtho/spotube/commit/a9a1d4c9dc24aaf3181dc4090d1822ebfe755991))
|
||||||
|
* **android:** audio issue when screen is off and broadcast audio session id ([#1221](https://github.com/krtirtho/spotube/issues/1221) & [#1247](https://github.com/krtirtho/spotube/issues/1247)) ([17105a6](https://github.com/krtirtho/spotube/commit/17105a640bf5107bd5d333b9b4d097c14a3949a2)), closes [KRTirtho/spotube#571](https://github.com/KRTirtho/spotube/issues/571)
|
||||||
|
* **android:** only ask battery optimization once [#1252](https://github.com/krtirtho/spotube/issues/1252) ([e516afb](https://github.com/krtirtho/spotube/commit/e516afb185f616471822ea745495a3d1d1281bd3))
|
||||||
|
* **android:** pressing back button in any other tab other than home exits the app ([c3289a0](https://github.com/krtirtho/spotube/commit/c3289a0ba4e7de094a15246677ffcb940504ebde))
|
||||||
|
* **android:** system back button in player page exits the app ([3294f65](https://github.com/krtirtho/spotube/commit/3294f657fe8a03b18d9be8974968b6508465963d))
|
||||||
|
* cleanTitle removing feat and ft from words instead of whole words ([8612345](https://github.com/krtirtho/spotube/commit/86123456f2ff577921cf62cffca180427dfe1dd5))
|
||||||
|
* friends list not scrollable with mouse drag ([ab08c82](https://github.com/krtirtho/spotube/commit/ab08c82c8dd501263049f3adcbd48907ba13e3a9))
|
||||||
|
* no draggable scrollbar in playlist/album page [#1158](https://github.com/krtirtho/spotube/issues/1158) ([6f71e52](https://github.com/krtirtho/spotube/commit/6f71e52ea8a5712d2c3527f2a524af9fbb718bef))
|
||||||
|
* non-banger songs breaking the queue if sources not found ([90f7c53](https://github.com/krtirtho/spotube/commit/90f7c531cdc8640afdbabf5a0592159715ea1e6f))
|
||||||
|
* track loading when not found in Youtube ([e964f61](https://github.com/krtirtho/spotube/commit/e964f61d38cb303e3d3fd60c866414f57207181c))
|
||||||
|
* **translations:** Update app_nl.arb ([#1168](https://github.com/krtirtho/spotube/issues/1168)) ([8167963](https://github.com/krtirtho/spotube/commit/8167963212eeb5dfb0b4fb2eadf81d466659a9f1))
|
||||||
|
|
||||||
## [3.4.1](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.4.1) (2024-01-27)
|
## [3.4.1](https://personal.github.com/krtirtho/spotube/compare/v3.4.0...v3.4.1) (2024-01-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
|||||||
- [Before Submitting an Enhancement](#before-submitting-an-enhancement)
|
- [Before Submitting an Enhancement](#before-submitting-an-enhancement)
|
||||||
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
|
- [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
|
||||||
- [Your First Code Contribution](#your-first-code-contribution)
|
- [Your First Code Contribution](#your-first-code-contribution)
|
||||||
- [Submit translations](#submit-translations)
|
- [Submit Translations](#submit-translations)
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
@ -123,16 +123,16 @@ Do the following:
|
|||||||
- Install Development dependencies in linux
|
- Install Development dependencies in linux
|
||||||
- Debian (>=12/Bookworm)/Ubuntu
|
- Debian (>=12/Bookworm)/Ubuntu
|
||||||
```bash
|
```bash
|
||||||
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev
|
$ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan
|
||||||
```
|
```
|
||||||
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
|
- Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04)
|
||||||
- Arch/Manjaro
|
- Arch/Manjaro
|
||||||
```bash
|
```bash
|
||||||
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify
|
yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan
|
||||||
```
|
```
|
||||||
- Fedora
|
- Fedora
|
||||||
```bash
|
```bash
|
||||||
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel
|
dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns
|
||||||
```
|
```
|
||||||
- Clone the Repo
|
- Clone the Repo
|
||||||
- Create a `.env` in root of the project following the `.env.example` template
|
- Create a `.env` in root of the project following the `.env.example` template
|
||||||
|
|||||||
1
Makefile
@ -28,6 +28,7 @@ publishaur:
|
|||||||
|
|
||||||
innoinstall:
|
innoinstall:
|
||||||
powershell curl -o build\installer.exe http://files.jrsoftware.org/is/6/innosetup-${INNO_VERSION}.exe
|
powershell curl -o build\installer.exe http://files.jrsoftware.org/is/6/innosetup-${INNO_VERSION}.exe
|
||||||
|
powershell git clone https://github.com/DomGries/InnoDependencyInstaller.git build\inno-depend
|
||||||
powershell build\installer.exe /verysilent /allusers /dir=build\iscc
|
powershell build\installer.exe /verysilent /allusers /dir=build\iscc
|
||||||
|
|
||||||
inno:
|
inno:
|
||||||
|
|||||||
56
README.md
@ -7,12 +7,14 @@ eliminating the need for Spotify Premium
|
|||||||
|
|
||||||
Btw it's not just another Electron app 😉
|
Btw it's not just another Electron app 😉
|
||||||
|
|
||||||
<a href="https://spotube.netlify.app"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
|
<a href="https://spotube.krtirtho.dev"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
|
||||||
<a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
|
<a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
|
||||||
|
|
||||||
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
|
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
|
||||||
<a href="https://www.buymeacoffee.com/krtirtho"><img alt="Buy me a Coffee" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg"></a>
|
<a href="https://www.buymeacoffee.com/krtirtho"><img alt="Buy me a Coffee" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/buymeacoffee-singular_vector.svg"></a>
|
||||||
|
|
||||||
|
[](https://news.ycombinator.com/item?id=39066136)
|
||||||
|
|
||||||
<a href="https://opencollective.com/spotube"><img src="https://opencollective.com/spotube/donate/button.png?color=blue" alt="Donate to our Open Collective" height="45"></a>
|
<a href="https://opencollective.com/spotube"><img src="https://opencollective.com/spotube/donate/button.png?color=blue" alt="Donate to our Open Collective" height="45"></a>
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -95,12 +97,7 @@ This handy table lists all the methods you can use to install Spotube:
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>AppImage</td>
|
<td>AppImage</td>
|
||||||
<td>
|
<td>AppImage's lacking stability led to it's temporal removal. More information at https://github.com/KRTirtho/spotube/issues/1082</td>
|
||||||
<a href="https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage">
|
|
||||||
<img width="220" alt="Download AppImage" src="https://user-images.githubusercontent.com/61944859/169455015-13385466-8901-48fe-ba90-b62d58b0be64.png">
|
|
||||||
</a>
|
|
||||||
<p><b>Note:</b> <a href="https://github.com/TheAssassin/AppImageLauncher">AppimageLauncher</a> is required!</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Debian/Ubuntu</td>
|
<td>Debian/Ubuntu</td>
|
||||||
@ -136,6 +133,15 @@ This handy table lists all the methods you can use to install Spotube:
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Macos - <a href="https://brew.sh">Homebrew</a></td>
|
||||||
|
<td>
|
||||||
|
<pre lang="bash">
|
||||||
|
brew tap krtirtho/apps
|
||||||
|
brew install --cask spotube
|
||||||
|
</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Windows - <a href="https://chocolatey.org">Chocolatey</a></td>
|
<td>Windows - <a href="https://chocolatey.org">Chocolatey</a></td>
|
||||||
<td>
|
<td>
|
||||||
@ -193,6 +199,8 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
1. [Piped](https://piped-docs.kavin.rocks/) - Piped is a privacy friendly alternative YouTube frontend, which is efficient and scalable by design.
|
||||||
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
1. [YouTube](https://youtube.com/) - YouTube is an American online video-sharing platform headquartered in San Bruno, California. Three former PayPal employees—Chad Hurley, Steve Chen, and Jawed Karim—created the service in February 2005
|
||||||
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
1. [JioSaavn](https://www.jiosaavn.com) - JioSaavn is an Indian online music streaming service and a digital distributor of Bollywood, English and other regional Indian music across the world. Since it was founded in 2007 as Saavn, the company has acquired rights to over 5 crore (50 million) music tracks in 15 languages
|
||||||
|
1. [SongLink](https://song.link) - SongLink is a free smart link service that helps you share music with your audience. It's a one-stop-shop for creating smart links for music, podcasts, and other audio content
|
||||||
|
1. [LRCLib](https://lrclib.net/) - A public synced lyric API
|
||||||
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
|
1. [Linux](https://www.linux.org) - Linux is a family of open-source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds. Linux is typically packaged in a Linux distribution
|
||||||
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
|
1. [AUR](https://aur.archlinux.org) - AUR stands for Arch User Repository. It is a community-driven repository for Arch-based Linux distributions users
|
||||||
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
|
1. [Flatpak](https://flatpak.org) - Flatpak is a utility for software deployment and package management for Linux
|
||||||
@ -221,9 +229,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration.
|
||||||
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
|
1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times.
|
||||||
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
1. [file_selector](https://pub.dev/packages/file_selector) - Flutter plugin for opening and saving files, or selecting directories, using native file selection UI.
|
||||||
1. [fl_query](https://fl-query.krtirtho.dev) - Asynchronous data caching, refetching & invalidation library for Flutter
|
|
||||||
1. [fl_query_hooks](https://fl-query.krtirtho.dev) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter
|
|
||||||
1. [fl_query_devtools](https://fl-query.krtirtho.dev) - Devtools support for Fl-Query
|
|
||||||
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft.
|
||||||
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite.
|
||||||
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices.
|
||||||
@ -231,7 +236,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse.
|
||||||
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.
|
||||||
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
1. [flutter_native_splash](https://pub.dev/packages/flutter_native_splash) - Customize Flutter's default white native splash screen with background color and splash image. Supports dark mode, full screen, and more.
|
||||||
1. [flutter_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
|
1. [flutter_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android.
|
||||||
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files.
|
||||||
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
1. [form_validator](https://github.com/TheMisir/form-validator) - Simplest form validation library for flutter's form field widgets
|
||||||
@ -240,12 +245,12 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling.
|
||||||
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256.
|
||||||
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps.
|
||||||
1. [hooks_riverpod](https://riverpod.dev) - A simple way to access state from anywhere in your application while robust and testable.
|
1. [hooks_riverpod](https://riverpod.dev) - A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
|
||||||
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
1. [html](https://pub.dev/packages/html) - APIs for parsing and manipulating HTML content outside the browser.
|
||||||
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
|
1. [http](https://pub.dev/packages/http) - A composable, multi-platform, Future-based API for HTTP requests.
|
||||||
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
|
1. [image_picker](https://pub.dev/packages/image_picker) - Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera.
|
||||||
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
|
1. [intl](https://pub.dev/packages/intl) - Contains code to deal with internationalized/localized messages, date and number formatting and parsing, bi-directional text, and other internationalization issues.
|
||||||
1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
|
1. [introduction_screen](https://pub.dev/packages/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities
|
||||||
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package.
|
||||||
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs.
|
||||||
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular.
|
||||||
@ -257,14 +262,12 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
|
1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web.
|
||||||
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories.
|
||||||
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.
|
||||||
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
|
||||||
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area.
|
||||||
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
|
1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter
|
||||||
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
|
1. [sidebarx](https://github.com/Frezyx/sidebarx) - flutter multiplatform navigation sidebar / side navigationbar / drawer widget
|
||||||
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
|
1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android.
|
||||||
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
|
1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community.
|
||||||
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet.
|
||||||
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
|
||||||
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
|
1. [stroke_text](https://github.com/MohamedAbd0/stroke_text) - A Simple Flutter plugin for applying stroke (border) style to a text widget
|
||||||
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
|
1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS
|
||||||
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
|
1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos.
|
||||||
@ -285,19 +288,34 @@ If you are concerned, you can [read the reason of choosing this license](https:/
|
|||||||
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
|
1. [wikipedia_api](https://github.com/KRTirtho/wikipedia_api) - Wikipedia API for dart and flutter
|
||||||
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
1. [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) - Converts already built widgets into skeleton loaders with no extra effort.
|
||||||
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
1. [app_links](https://github.com/llfbandit/app_links) - Android App Links, Deep Links, iOs Universal Links and Custom URL schemes handler for Flutter (desktop included).
|
||||||
1. [win32_registry](https://win32.pub) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
1. [win32_registry](https://pub.dev/packages/win32_registry) - A package that provides a friendly Dart API for accessing the Windows Registry.
|
||||||
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
1. [flutter_sharing_intent](https://github.com/bhagat-techind/flutter_sharing_intent.git) - A flutter plugin that allow flutter apps to receive photos, videos, text, urls or any other file types from another app.
|
||||||
|
1. [flutter_broadcasts](https://pub.dev/packages/flutter_broadcasts) - A plugin for sending and receiving broadcasts with Android intents and iOS notifications.
|
||||||
|
1. [freezed_annotation](https://pub.dev/packages/freezed_annotation) - Annotations for the freezed code-generator. This package does nothing without freezed too.
|
||||||
|
1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API.
|
||||||
|
1. [bonsoir](https://bonsoir.skyost.eu) - A Zeroconf library that allows you to discover network services and to broadcast your own. Based on Apple Bonjour and Android NSD.
|
||||||
|
1. [shelf](https://pub.dev/packages/shelf) - A model for web server middleware that encourages composition and easy reuse.
|
||||||
|
1. [shelf_router](https://pub.dev/packages/shelf_router) - A convenient request router for the shelf web-framework, with support for URL-parameters, nested routers and routers generated from source annotations.
|
||||||
|
1. [shelf_web_socket](https://pub.dev/packages/shelf_web_socket) - A shelf handler that wires up a listener for every connection.
|
||||||
|
1. [web_socket_channel](https://pub.dev/packages/web_socket_channel) - StreamChannel wrappers for WebSockets. Provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel.
|
||||||
|
1. [lrc](https://pub.dev/packages/lrc) - A Dart-only package that creates, parses, and handles LRC, which is a format that stores song lyrics.
|
||||||
|
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
||||||
|
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
|
||||||
|
1. [timezone](https://pub.dev/packages/timezone) - Time zone database and time zone aware DateTime.
|
||||||
|
1. [crypto](https://pub.dev/packages/crypto) - Implementations of SHA, MD5, and HMAC cryptographic functions.
|
||||||
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation.
|
||||||
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied.
|
||||||
1. [flutter_distributor](https://distributor.leanflutter.org) - A complete tool for packaging and publishing your Flutter apps.
|
1. [flutter_distributor](https://distributor.leanflutter.dev) - A complete tool for packaging and publishing your Flutter apps.
|
||||||
1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
|
1. [flutter_gen_runner](https://github.com/FlutterGen/flutter_gen) - The Flutter code generator for your assets, fonts, colors, … — Get rid of all String-based APIs.
|
||||||
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
|
1. [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon.
|
||||||
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
|
1. [flutter_lints](https://pub.dev/packages/flutter_lints) - Recommended lints for Flutter apps, packages, and plugins to encourage good coding practices.
|
||||||
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
|
1. [hive_generator](https://github.com/hivedb/hive/tree/master/hive_generator) - Extension for Hive. Automatically generates TypeAdapters to store any class.
|
||||||
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
1. [json_serializable](https://pub.dev/packages/json_serializable) - Automatically generate code for converting to and from JSON by annotating Dart classes.
|
||||||
1. [pub_api_client](https://github.com/leoafarias/pub_api_client) - An API Client for Pub to interact with public package information.
|
1. [freezed](https://pub.dev/packages/freezed) - Code generation for immutable classes that has a simple syntax/API without compromising on the features.
|
||||||
1. [pubspec_parse](https://pub.dev/packages/pubspec_parse) - Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting.
|
1. [custom_lint](https://pub.dev/packages/custom_lint) - Lint rules are a powerful way to improve the maintainability of a project. Custom Lint allows package authors and developers to easily write custom lint rules.
|
||||||
|
1. [riverpod_lint](https://riverpod.dev) - Riverpod_lint is a developer tool for users of Riverpod, designed to help stop common issues and simplify repetitive tasks.
|
||||||
1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
|
1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development
|
||||||
|
1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video
|
||||||
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
1. [scrobblenaut](https://github.com/Nebulino/Scrobblenaut) - A deadly simple LastFM API Wrapper for Dart. So deadly simple that it's gonna hit the mark.
|
||||||
1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
|
1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter.
|
||||||
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
1. [draggable_scrollbar](https://github.com/fluttercommunity/flutter-draggable-scrollbar) - A scrollbar that can be dragged for quickly navigation through a vertical list. Additional option is showing label next to scrollthumb with information about current item.
|
||||||
|
|||||||
@ -25,10 +25,17 @@ linter:
|
|||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
file_names: false
|
file_names: false
|
||||||
|
avoid_renaming_method_parameters: false
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
analyzer:
|
analyzer:
|
||||||
enable-experiment:
|
errors:
|
||||||
- records
|
invalid_annotation_target: ignore
|
||||||
- patterns
|
plugins:
|
||||||
|
- custom_lint
|
||||||
|
exclude:
|
||||||
|
- "**.freezed.dart"
|
||||||
|
- "**.g.dart"
|
||||||
|
- "**.gr.dart"
|
||||||
|
- "**/generated_plugin_registrant.dart"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
@ -5,6 +5,10 @@
|
|||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
|
<item name="android:windowFullscreen">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/logos/songlink-transparent.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/logos/songlink.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
28
bin/translated_messages.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
final translatedFile =
|
||||||
|
jsonDecode(await File('tm.json').readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
for (final MapEntry(:key, :value) in translatedFile.entries) {
|
||||||
|
print('Updating locale: $key');
|
||||||
|
final file = File('lib/l10n/app_$key.arb');
|
||||||
|
|
||||||
|
final fileContent =
|
||||||
|
jsonDecode(await file.readAsString()) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
final newContent = {
|
||||||
|
...fileContent,
|
||||||
|
...value,
|
||||||
|
};
|
||||||
|
|
||||||
|
await file.writeAsString(
|
||||||
|
const JsonEncoder.withIndent(' ').convert(newContent),
|
||||||
|
);
|
||||||
|
|
||||||
|
print('✅ Updated locale: $key');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@ -40,7 +42,6 @@ void main(List<String> args) {
|
|||||||
"Translate following to their appropriate locale for flutter arb translations files."
|
"Translate following to their appropriate locale for flutter arb translations files."
|
||||||
" Put the respective new translations in a map of their corresponding locale.",
|
" Put the respective new translations in a map of their corresponding locale.",
|
||||||
);
|
);
|
||||||
// ignore: avoid_print
|
|
||||||
print(
|
print(
|
||||||
const JsonEncoder.withIndent(' ').convert(
|
const JsonEncoder.withIndent(' ').convert(
|
||||||
args.isNotEmpty ? messagesWithValues[args.first] : messagesWithValues,
|
args.isNotEmpty ? messagesWithValues[args.first] : messagesWithValues,
|
||||||
|
|||||||
@ -21,6 +21,6 @@
|
|||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>MinimumOSVersion</key>
|
<key>MinimumOSVersion</key>
|
||||||
<string>11.0</string>
|
<string>12.0</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# Uncomment this line to define a global platform for your project
|
||||||
# platform :ios, '11.0'
|
platform :ios, '13.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|||||||
@ -5,6 +5,9 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- audio_session (0.0.1):
|
- audio_session (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- bonsoir_darwin (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
- device_info_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- DKImagePickerController/Core (4.3.4):
|
- DKImagePickerController/Core (4.3.4):
|
||||||
@ -44,11 +47,13 @@ PODS:
|
|||||||
- file_selector_ios (0.0.1):
|
- file_selector_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_inappwebview (0.0.1):
|
- flutter_broadcasts (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_inappwebview/Core (= 0.0.1)
|
- flutter_inappwebview_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- flutter_inappwebview_ios/Core (= 0.0.1)
|
||||||
- OrderedSet (~> 5.0)
|
- OrderedSet (~> 5.0)
|
||||||
- flutter_inappwebview/Core (0.0.1):
|
- flutter_inappwebview_ios/Core (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- OrderedSet (~> 5.0)
|
- OrderedSet (~> 5.0)
|
||||||
- flutter_keyboard_visibility (0.0.1):
|
- flutter_keyboard_visibility (0.0.1):
|
||||||
@ -102,11 +107,13 @@ DEPENDENCIES:
|
|||||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||||
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||||
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||||
|
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||||
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
|
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
||||||
|
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
@ -142,6 +149,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/audio_service/ios"
|
:path: ".symlinks/plugins/audio_service/ios"
|
||||||
audio_session:
|
audio_session:
|
||||||
:path: ".symlinks/plugins/audio_session/ios"
|
:path: ".symlinks/plugins/audio_session/ios"
|
||||||
|
bonsoir_darwin:
|
||||||
|
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
@ -150,8 +159,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/file_selector_ios/ios"
|
:path: ".symlinks/plugins/file_selector_ios/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_inappwebview:
|
flutter_broadcasts:
|
||||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
:path: ".symlinks/plugins/flutter_broadcasts/ios"
|
||||||
|
flutter_inappwebview_ios:
|
||||||
|
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||||
flutter_keyboard_visibility:
|
flutter_keyboard_visibility:
|
||||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||||
flutter_mailer:
|
flutter_mailer:
|
||||||
@ -191,13 +202,15 @@ SPEC CHECKSUMS:
|
|||||||
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
|
||||||
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||||
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||||
|
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||||
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
|
||||||
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
file_selector_ios: 8c25d700d625e1dcdd6599f2d927072f2254647b
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
|
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||||
|
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
|
||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
@ -221,6 +234,6 @@ SPEC CHECKSUMS:
|
|||||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||||
|
|
||||||
PODFILE CHECKSUM: e36c7ad9836dfd8d22934c7680185432a658e28f
|
PODFILE CHECKSUM: 0659b64ac6e9e96b61d8550decffa8bff51a957e
|
||||||
|
|
||||||
COCOAPODS: 1.14.3
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@ -406,7 +406,7 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
@ -1056,6 +1056,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1078,6 +1079,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1099,6 +1101,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1198,6 +1201,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1294,6 +1298,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1387,6 +1392,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1408,6 +1414,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1430,6 +1437,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1452,6 +1460,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1473,6 +1482,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1494,6 +1504,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1515,6 +1526,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1614,6 +1626,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1636,6 +1649,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1732,6 +1746,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1753,6 +1768,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1846,6 +1862,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1867,6 +1884,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1888,6 +1906,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1910,6 +1929,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1932,6 +1952,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1954,6 +1975,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1975,6 +1997,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -1996,6 +2019,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2017,6 +2041,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2038,6 +2063,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2059,6 +2085,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2158,6 +2185,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2180,6 +2208,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2202,6 +2231,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2298,6 +2328,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2319,6 +2350,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2340,6 +2372,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2433,6 +2466,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "stable-Info.plist";
|
INFOPLIST_FILE = "stable-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2454,6 +2488,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "dev-Info.plist";
|
INFOPLIST_FILE = "dev-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@ -2475,6 +2510,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-nightly";
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = 88NVGSJ5N3;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = "nightly-Info.plist";
|
INFOPLIST_FILE = "nightly-Info.plist";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
@ -1,66 +1,76 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<true />
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>Spotube</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>Spotube</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>6.0</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>spotube</string>
|
<string>6.0</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleName</key>
|
||||||
<string>APPL</string>
|
<string>spotube</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>????</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>????</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>CFBundleVersion</key>
|
||||||
<true/>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>LaunchScreen</string>
|
<true />
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<string>Main</string>
|
<dict>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<array>
|
<true />
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<true />
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
</dict>
|
||||||
</array>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>This app require access to the device camera</string>
|
||||||
<array>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>This app does not require access to the device microphone</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>This app require access to the photo library</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
</array>
|
<true />
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIBackgroundModes</key>
|
||||||
<true/>
|
<array>
|
||||||
<key>NSAppTransportSecurity</key>
|
<string>audio</string>
|
||||||
<dict>
|
</array>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<true/>
|
<string>LaunchScreen</string>
|
||||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<true/>
|
<string>Main</string>
|
||||||
</dict>
|
<key>UIStatusBarHidden</key>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<false />
|
||||||
<true/>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<key>UIStatusBarHidden</key>
|
<array>
|
||||||
<false/>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>This app require access to the photo library</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
</array>
|
||||||
<string>This app require access to the device camera</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<array>
|
||||||
<string>This app does not require access to the device microphone</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<true/>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</dict>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</plist>
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<true />
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>To allow other devices on the network control playback of Spotube securely.</string>
|
||||||
|
<key>NSBonjourServices</key>
|
||||||
|
<array>
|
||||||
|
<string>_spotube._tcp</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -41,6 +41,10 @@
|
|||||||
<string>This app require access to the photo library</string>
|
<string>This app require access to the photo library</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
@ -1,66 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Spotube</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>spotube</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Spotube</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>spotube</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
</dict>
|
||||||
<dict>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<string>This app require access to the device camera</string>
|
||||||
<true/>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
<string>This app does not require access to the device microphone</string>
|
||||||
<true/>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
</dict>
|
<string>This app require access to the photo library</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIBackgroundModes</key>
|
||||||
<false/>
|
<array>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>audio</string>
|
||||||
<string>This app require access to the photo library</string>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>This app require access to the device camera</string>
|
<string>LaunchScreen</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>This app does not require access to the device microphone</string>
|
<string>Main</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,66 +1,70 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Spotube</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>spotube</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Spotube</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>spotube</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>NSAllowsArbitraryLoadsForMedia</key>
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
</dict>
|
||||||
<dict>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<string>This app require access to the device camera</string>
|
||||||
<true/>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<key>NSAllowsArbitraryLoadsForMedia</key>
|
<string>This app does not require access to the device microphone</string>
|
||||||
<true/>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
</dict>
|
<string>This app require access to the photo library</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIBackgroundModes</key>
|
||||||
<false/>
|
<array>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>audio</string>
|
||||||
<string>This app require access to the photo library</string>
|
</array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>This app require access to the device camera</string>
|
<string>LaunchScreen</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>This app does not require access to the device microphone</string>
|
<string>Main</string>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIStatusBarHidden</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -9,6 +9,21 @@
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class $AssetsLogosGen {
|
||||||
|
const $AssetsLogosGen();
|
||||||
|
|
||||||
|
/// File path: assets/logos/songlink-transparent.png
|
||||||
|
AssetGenImage get songlinkTransparent =>
|
||||||
|
const AssetGenImage('assets/logos/songlink-transparent.png');
|
||||||
|
|
||||||
|
/// File path: assets/logos/songlink.png
|
||||||
|
AssetGenImage get songlink =>
|
||||||
|
const AssetGenImage('assets/logos/songlink.png');
|
||||||
|
|
||||||
|
/// List of all assets
|
||||||
|
List<AssetGenImage> get values => [songlinkTransparent, songlink];
|
||||||
|
}
|
||||||
|
|
||||||
class $AssetsTutorialGen {
|
class $AssetsTutorialGen {
|
||||||
const $AssetsTutorialGen();
|
const $AssetsTutorialGen();
|
||||||
|
|
||||||
@ -37,6 +52,7 @@ class Assets {
|
|||||||
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
||||||
static const AssetGenImage likedTracks =
|
static const AssetGenImage likedTracks =
|
||||||
AssetGenImage('assets/liked-tracks.jpg');
|
AssetGenImage('assets/liked-tracks.jpg');
|
||||||
|
static const $AssetsLogosGen logos = $AssetsLogosGen();
|
||||||
static const AssetGenImage placeholder =
|
static const AssetGenImage placeholder =
|
||||||
AssetGenImage('assets/placeholder.png');
|
AssetGenImage('assets/placeholder.png');
|
||||||
static const AssetGenImage spotubeHeroBanner =
|
static const AssetGenImage spotubeHeroBanner =
|
||||||
@ -72,7 +88,7 @@ class Assets {
|
|||||||
AssetGenImage('assets/user-placeholder.png');
|
AssetGenImage('assets/user-placeholder.png');
|
||||||
|
|
||||||
/// List of all assets
|
/// List of all assets
|
||||||
List<dynamic> get values => [
|
static List<dynamic> get values => [
|
||||||
albumPlaceholder,
|
albumPlaceholder,
|
||||||
bengaliPatternsBg,
|
bengaliPatternsBg,
|
||||||
branding,
|
branding,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
import 'package:spotube/models/spotify/home_feed.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
|
|
||||||
abstract class FakeData {
|
abstract class FakeData {
|
||||||
static final Image image = Image()
|
static final Image image = Image()
|
||||||
..height = 1
|
..height = 1
|
||||||
..width = 1
|
..width = 1
|
||||||
..url = "url";
|
..url = "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg";
|
||||||
|
|
||||||
static final Followers followers = Followers()
|
static final Followers followers = Followers()
|
||||||
..href = "text"
|
..href = "text"
|
||||||
@ -196,4 +197,30 @@ abstract class FakeData {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final feedSection = SpotifyHomeFeedSection(
|
||||||
|
typename: "HomeGenericSectionData",
|
||||||
|
uri: "spotify:section:lol",
|
||||||
|
title: "Dummy",
|
||||||
|
items: [
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
SpotifyHomeFeedSectionItem(
|
||||||
|
typename: "PlaylistResponseWrapper",
|
||||||
|
playlist: SpotifySectionPlaylist(
|
||||||
|
name: "Playlist $i",
|
||||||
|
description: "Really super important description $i",
|
||||||
|
format: "daily-mix",
|
||||||
|
images: [
|
||||||
|
const SpotifySectionItemImage(
|
||||||
|
height: 1,
|
||||||
|
width: 1,
|
||||||
|
url: "https://dummyimage.com/100x100/cfcfcf/cfcfcf.jpg",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
owner: "Spotify",
|
||||||
|
uri: "spotify:playlist:id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:spotube/components/player/player_controls.dart';
|
|
||||||
import 'package:spotube/collections/routes.dart';
|
import 'package:spotube/collections/routes.dart';
|
||||||
|
import 'package:spotube/components/player/player_controls.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
@ -64,6 +64,7 @@ class HomeTabIntent extends Intent {
|
|||||||
class HomeTabAction extends Action<HomeTabIntent> {
|
class HomeTabAction extends Action<HomeTabIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) {
|
invoke(intent) {
|
||||||
|
final router = intent.ref.read(routerProvider);
|
||||||
switch (intent.tab) {
|
switch (intent.tab) {
|
||||||
case HomeTabs.browse:
|
case HomeTabs.browse:
|
||||||
router.go("/");
|
router.go("/");
|
||||||
@ -91,7 +92,7 @@ class SeekIntent extends Intent {
|
|||||||
class SeekAction extends Action<SeekIntent> {
|
class SeekAction extends Action<SeekIntent> {
|
||||||
@override
|
@override
|
||||||
invoke(intent) async {
|
invoke(intent) async {
|
||||||
final playlist = intent.ref.read(ProxyPlaylistNotifier.provider);
|
final playlist = intent.ref.read(proxyPlaylistProvider);
|
||||||
if (playlist.isFetching) {
|
if (playlist.isFetching) {
|
||||||
DirectionalFocusAction().invoke(
|
DirectionalFocusAction().invoke(
|
||||||
DirectionalFocusIntent(
|
DirectionalFocusIntent(
|
||||||
|
|||||||
@ -6,6 +6,11 @@ class ISOLanguageName {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.nativeName,
|
required this.nativeName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "$name ($nativeName)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncomment the languages as we add support for them
|
// Uncomment the languages as we add support for them
|
||||||
@ -152,10 +157,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Croatian",
|
// name: "Croatian",
|
||||||
// nativeName: "hrvatski",
|
// nativeName: "hrvatski",
|
||||||
// ),
|
// ),
|
||||||
// "cs": const ISOLanguageName(
|
"cs": const ISOLanguageName(
|
||||||
// name: "Czech",
|
name: "Czech",
|
||||||
// nativeName: "česky, čeština",
|
nativeName: "česky, čeština",
|
||||||
// ),
|
),
|
||||||
// "da": const ISOLanguageName(
|
// "da": const ISOLanguageName(
|
||||||
// name: "Danish",
|
// name: "Danish",
|
||||||
// nativeName: "dansk",
|
// nativeName: "dansk",
|
||||||
@ -348,10 +353,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Kongo",
|
// name: "Kongo",
|
||||||
// nativeName: "KiKongo",
|
// nativeName: "KiKongo",
|
||||||
// ),
|
// ),
|
||||||
// "ko": const ISOLanguageName(
|
"ko": const ISOLanguageName(
|
||||||
// name: "Korean",
|
name: "Korean",
|
||||||
// nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
|
nativeName: "한국어 (韓國語), 조선말 (朝鮮語)",
|
||||||
// ),
|
),
|
||||||
// "ku": const ISOLanguageName(
|
// "ku": const ISOLanguageName(
|
||||||
// name: "Kurdish",
|
// name: "Kurdish",
|
||||||
// nativeName: "Kurdî, كوردی",
|
// nativeName: "Kurdî, كوردی",
|
||||||
@ -632,10 +637,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Tajik",
|
// name: "Tajik",
|
||||||
// nativeName: "тоҷикӣ, toğikī, تاجیکی",
|
// nativeName: "тоҷикӣ, toğikī, تاجیکی",
|
||||||
// ),
|
// ),
|
||||||
// "th": const ISOLanguageName(
|
"th": const ISOLanguageName(
|
||||||
// name: "Thai",
|
name: "Thai",
|
||||||
// nativeName: "ไทย",
|
nativeName: "ไทย",
|
||||||
// ),
|
),
|
||||||
// "ti": const ISOLanguageName(
|
// "ti": const ISOLanguageName(
|
||||||
// name: "Tigrinya",
|
// name: "Tigrinya",
|
||||||
// nativeName: "ትግርኛ",
|
// nativeName: "ትግርኛ",
|
||||||
@ -700,10 +705,10 @@ abstract class LanguageLocals {
|
|||||||
// name: "Venda",
|
// name: "Venda",
|
||||||
// nativeName: "Tshivenḓa",
|
// nativeName: "Tshivenḓa",
|
||||||
// ),
|
// ),
|
||||||
// "vi": const ISOLanguageName(
|
"vi": const ISOLanguageName(
|
||||||
// name: "Vietnamese",
|
name: "Vietnamese",
|
||||||
// nativeName: "Tiếng Việt",
|
nativeName: "Tiếng Việt",
|
||||||
// ),
|
),
|
||||||
// "vo": const ISOLanguageName(
|
// "vo": const ISOLanguageName(
|
||||||
// name: "Volapük",
|
// name: "Volapük",
|
||||||
// nativeName: "Volapük",
|
// nativeName: "Volapük",
|
||||||
|
|||||||
@ -2,8 +2,14 @@ import 'package:catcher_2/catcher_2.dart';
|
|||||||
import 'package:flutter/foundation.dart' hide Category;
|
import 'package:flutter/foundation.dart' hide Category;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Search;
|
import 'package:spotify/spotify.dart' hide Search;
|
||||||
|
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||||
import 'package:spotube/pages/album/album.dart';
|
import 'package:spotube/pages/album/album.dart';
|
||||||
|
import 'package:spotube/pages/connect/connect.dart';
|
||||||
|
import 'package:spotube/pages/connect/control/control.dart';
|
||||||
|
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||||
|
import 'package:spotube/pages/home/feed/feed_section.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||||
import 'package:spotube/pages/home/genres/genres.dart';
|
import 'package:spotube/pages/home/genres/genres.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
@ -13,11 +19,14 @@ import 'package:spotube/pages/library/playlist_generate/playlist_generate_result
|
|||||||
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
|
||||||
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
import 'package:spotube/pages/playlist/liked_playlist.dart';
|
||||||
import 'package:spotube/pages/playlist/playlist.dart';
|
import 'package:spotube/pages/playlist/playlist.dart';
|
||||||
|
import 'package:spotube/pages/profile/profile.dart';
|
||||||
import 'package:spotube/pages/search/search.dart';
|
import 'package:spotube/pages/search/search.dart';
|
||||||
import 'package:spotube/pages/settings/blacklist.dart';
|
import 'package:spotube/pages/settings/blacklist.dart';
|
||||||
import 'package:spotube/pages/settings/about.dart';
|
import 'package:spotube/pages/settings/about.dart';
|
||||||
import 'package:spotube/pages/settings/logs.dart';
|
import 'package:spotube/pages/settings/logs.dart';
|
||||||
import 'package:spotube/pages/track/track.dart';
|
import 'package:spotube/pages/track/track.dart';
|
||||||
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/components/shared/spotube_page_route.dart';
|
import 'package:spotube/components/shared/spotube_page_route.dart';
|
||||||
import 'package:spotube/pages/artist/artist.dart';
|
import 'package:spotube/pages/artist/artist.dart';
|
||||||
@ -31,157 +40,207 @@ import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
|||||||
|
|
||||||
final rootNavigatorKey = Catcher2.navigatorKey;
|
final rootNavigatorKey = Catcher2.navigatorKey;
|
||||||
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
final router = GoRouter(
|
final routerProvider = Provider((ref) {
|
||||||
navigatorKey: rootNavigatorKey,
|
return GoRouter(
|
||||||
routes: [
|
navigatorKey: rootNavigatorKey,
|
||||||
ShellRoute(
|
routes: [
|
||||||
navigatorKey: shellRouteNavigatorKey,
|
ShellRoute(
|
||||||
builder: (context, state, child) => RootApp(child: child),
|
navigatorKey: shellRouteNavigatorKey,
|
||||||
routes: [
|
builder: (context, state, child) => RootApp(child: child),
|
||||||
GoRoute(
|
routes: [
|
||||||
path: "/",
|
GoRoute(
|
||||||
pageBuilder: (context, state) => const SpotubePage(child: HomePage()),
|
path: "/",
|
||||||
routes: [
|
redirect: (context, state) async {
|
||||||
GoRoute(
|
final authNotifier = ref.read(authenticationProvider.notifier);
|
||||||
path: "genres",
|
final json = await authNotifier.box.get(authNotifier.cacheKey);
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: GenrePage()),
|
if (json?["cookie"] == null &&
|
||||||
),
|
!KVStoreService.doneGettingStarted) {
|
||||||
GoRoute(
|
return "/getting-started";
|
||||||
path: "genre/:categoryId",
|
}
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: GenrePlaylistsPage(
|
return null;
|
||||||
category: state.extra as Category,
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/search",
|
|
||||||
name: "Search",
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: SearchPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/library",
|
|
||||||
name: "Library",
|
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: LibraryPage()),
|
const SpotubePage(child: HomePage()),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "generate",
|
path: "genres",
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) =>
|
||||||
const SpotubePage(child: PlaylistGeneratorPage()),
|
const SpotubePage(child: GenrePage()),
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "result",
|
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
|
||||||
child: PlaylistGenerateResultPage(
|
|
||||||
state:
|
|
||||||
state.extra as PlaylistGenerateResultRouteState,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
GoRoute(
|
|
||||||
path: "/lyrics",
|
|
||||||
name: "Lyrics",
|
|
||||||
pageBuilder: (context, state) =>
|
|
||||||
const SpotubePage(child: LyricsPage()),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: "/settings",
|
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
|
||||||
child: SettingsPage(),
|
|
||||||
),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
path: "blacklist",
|
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
|
||||||
child: const BlackListPage(),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (!kIsWeb)
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "logs",
|
path: "genre/:categoryId",
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: const LogsPage(),
|
child: GenrePlaylistsPage(
|
||||||
|
category: state.extra as Category,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: "about",
|
path: "feeds/:feedId",
|
||||||
pageBuilder: (context, state) => SpotubeSlidePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: const AboutSpotube(),
|
child: HomeFeedSectionPage(
|
||||||
),
|
sectionUri: state.pathParameters["feedId"] as String,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/search",
|
||||||
|
name: "Search",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: SearchPage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/library",
|
||||||
|
name: "Library",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: LibraryPage()),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: "generate",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: PlaylistGeneratorPage()),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: "result",
|
||||||
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
|
child: PlaylistGenerateResultPage(
|
||||||
|
state: state.extra as GeneratePlaylistProviderInput,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
GoRoute(
|
||||||
|
path: "/lyrics",
|
||||||
|
name: "Lyrics",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: LyricsPage()),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/settings",
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: SettingsPage(),
|
||||||
),
|
),
|
||||||
],
|
routes: [
|
||||||
),
|
GoRoute(
|
||||||
GoRoute(
|
path: "blacklist",
|
||||||
path: "/album/:id",
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
pageBuilder: (context, state) {
|
child: const BlackListPage(),
|
||||||
assert(state.extra is AlbumSimple);
|
),
|
||||||
return SpotubePage(
|
),
|
||||||
child: AlbumPage(album: state.extra as AlbumSimple),
|
if (!kIsWeb)
|
||||||
);
|
GoRoute(
|
||||||
},
|
path: "logs",
|
||||||
),
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
GoRoute(
|
child: const LogsPage(),
|
||||||
path: "/artist/:id",
|
),
|
||||||
pageBuilder: (context, state) {
|
),
|
||||||
assert(state.pathParameters["id"] != null);
|
GoRoute(
|
||||||
return SpotubePage(child: ArtistPage(state.pathParameters["id"]!));
|
path: "about",
|
||||||
},
|
pageBuilder: (context, state) => SpotubeSlidePage(
|
||||||
),
|
child: const AboutSpotube(),
|
||||||
GoRoute(
|
),
|
||||||
path: "/playlist/:id",
|
),
|
||||||
pageBuilder: (context, state) {
|
],
|
||||||
assert(state.extra is PlaylistSimple);
|
),
|
||||||
return SpotubePage(
|
GoRoute(
|
||||||
child: state.pathParameters["id"] == "user-liked-tracks"
|
path: "/album/:id",
|
||||||
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
|
pageBuilder: (context, state) {
|
||||||
: PlaylistPage(playlist: state.extra as PlaylistSimple),
|
assert(state.extra is AlbumSimple);
|
||||||
);
|
return SpotubePage(
|
||||||
},
|
child: AlbumPage(album: state.extra as AlbumSimple),
|
||||||
),
|
);
|
||||||
GoRoute(
|
},
|
||||||
path: "/track/:id",
|
),
|
||||||
pageBuilder: (context, state) {
|
GoRoute(
|
||||||
final id = state.pathParameters["id"]!;
|
path: "/artist/:id",
|
||||||
return SpotubePage(
|
pageBuilder: (context, state) {
|
||||||
child: TrackPage(trackId: id),
|
assert(state.pathParameters["id"] != null);
|
||||||
);
|
return SpotubePage(
|
||||||
},
|
child: ArtistPage(state.pathParameters["id"]!));
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
),
|
GoRoute(
|
||||||
GoRoute(
|
path: "/playlist/:id",
|
||||||
path: "/mini-player",
|
pageBuilder: (context, state) {
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
assert(state.extra is PlaylistSimple);
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
return SpotubePage(
|
||||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
child: state.pathParameters["id"] == "user-liked-tracks"
|
||||||
|
? LikedPlaylistPage(playlist: state.extra as PlaylistSimple)
|
||||||
|
: PlaylistPage(playlist: state.extra as PlaylistSimple),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/track/:id",
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final id = state.pathParameters["id"]!;
|
||||||
|
return SpotubePage(
|
||||||
|
child: TrackPage(trackId: id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/connect",
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: ConnectPage(),
|
||||||
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: "control",
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
return const SpotubePage(
|
||||||
|
child: ConnectControlPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/profile",
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: ProfilePage()),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
GoRoute(
|
||||||
GoRoute(
|
path: "/mini-player",
|
||||||
path: "/login",
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||||
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
),
|
||||||
),
|
),
|
||||||
),
|
GoRoute(
|
||||||
GoRoute(
|
path: "/getting-started",
|
||||||
path: "/login-tutorial",
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
pageBuilder: (context, state) => const SpotubePage(
|
child: GettingStarting(),
|
||||||
child: LoginTutorial(),
|
),
|
||||||
),
|
),
|
||||||
),
|
GoRoute(
|
||||||
GoRoute(
|
path: "/login",
|
||||||
path: "/lastfm-login",
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
parentNavigatorKey: rootNavigatorKey,
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
pageBuilder: (context, state) =>
|
child: kIsMobile ? const WebViewLogin() : const DesktopLoginPage(),
|
||||||
const SpotubePage(child: LastFMLoginPage()),
|
),
|
||||||
),
|
),
|
||||||
],
|
GoRoute(
|
||||||
);
|
path: "/login-tutorial",
|
||||||
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
|
pageBuilder: (context, state) => const SpotubePage(
|
||||||
|
child: LoginTutorial(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: "/lastfm-login",
|
||||||
|
parentNavigatorKey: rootNavigatorKey,
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
const SpotubePage(child: LastFMLoginPage()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -111,4 +111,14 @@ abstract class SpotubeIcons {
|
|||||||
static const wikipedia = SimpleIcons.wikipedia;
|
static const wikipedia = SimpleIcons.wikipedia;
|
||||||
static const discord = SimpleIcons.discord;
|
static const discord = SimpleIcons.discord;
|
||||||
static const youtube = SimpleIcons.youtube;
|
static const youtube = SimpleIcons.youtube;
|
||||||
|
static const radio = FeatherIcons.radio;
|
||||||
|
static const github = SimpleIcons.github;
|
||||||
|
static const openCollective = SimpleIcons.opencollective;
|
||||||
|
static const anonymous = FeatherIcons.user;
|
||||||
|
static const history = FeatherIcons.clock;
|
||||||
|
static const connect = FeatherIcons.link;
|
||||||
|
static const speaker = FeatherIcons.speaker;
|
||||||
|
static const monitor = FeatherIcons.monitor;
|
||||||
|
static const power = FeatherIcons.power;
|
||||||
|
static const bluetooth = FeatherIcons.bluetooth;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/infinite_query.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/extensions/track.dart';
|
||||||
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/queries/album.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
extension FormattedAlbumType on AlbumType {
|
extension FormattedAlbumType on AlbumType {
|
||||||
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
|
String get formatted => name.replaceFirst(name[0], name[0].toUpperCase());
|
||||||
@ -21,17 +23,15 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final AlbumSimple album;
|
final AlbumSimple album;
|
||||||
const AlbumCard(
|
const AlbumCard(
|
||||||
this.album, {
|
this.album, {
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
|
||||||
final queryClient = useQueryClient();
|
|
||||||
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() => playlist.containsCollection(album.id!),
|
() => playlist.containsCollection(album.id!),
|
||||||
@ -39,39 +39,19 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
|
||||||
|
|
||||||
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
|
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTrack() async {
|
Future<List<Track>> fetchAllTrack() async {
|
||||||
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
||||||
return album.tracks!
|
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
||||||
.map((track) =>
|
|
||||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
final job = AlbumQueries.tracksOfJob(album.id!);
|
await ref.read(albumTracksProvider(album).future);
|
||||||
|
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
||||||
final query = queryClient.createInfiniteQuery(
|
|
||||||
job.queryKey,
|
|
||||||
(page) => job.task(page, (spotify: spotify, album: album)),
|
|
||||||
initialPage: 0,
|
|
||||||
nextPage: job.nextPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await query.fetchAllTracks(
|
|
||||||
getAllTracks: () async {
|
|
||||||
final res = await spotify.albums.tracks(album.id!).all();
|
|
||||||
return res
|
|
||||||
.map((e) => TypeConversionUtils.simpleTrack_X_Track(e, album))
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(
|
imageUrl: album.images.asUrlString(
|
||||||
album.images,
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
@ -80,7 +60,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
updating.value,
|
updating.value,
|
||||||
title: album.name!,
|
title: album.name!,
|
||||||
description:
|
description:
|
||||||
"${album.albumType?.formatted} • ${TypeConversionUtils.artists_X_String<ArtistSimple>(album.artists ?? [])}",
|
"${album.albumType?.formatted} • ${album.artists?.asString() ?? ""}",
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(context, "/album/${album.id}", extra: album);
|
ServiceUtils.push(context, "/album/${album.id}", extra: album);
|
||||||
},
|
},
|
||||||
@ -93,10 +73,21 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
final fetchedTracks = await fetchAllTrack();
|
final fetchedTracks = await fetchAllTrack();
|
||||||
|
|
||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty || !context.mounted) return;
|
||||||
|
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
playlistNotifier.addCollection(album.id!);
|
if (isRemoteDevice) {
|
||||||
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
|
await remotePlayback.load(
|
||||||
|
WebSocketLoadEventData(
|
||||||
|
tracks: fetchedTracks,
|
||||||
|
collectionId: album.id!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
|
playlistNotifier.addCollection(album.id!);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,35 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class ArtistAlbumList extends HookConsumerWidget {
|
class ArtistAlbumList extends HookConsumerWidget {
|
||||||
final String artistId;
|
final String artistId;
|
||||||
ArtistAlbumList(
|
ArtistAlbumList(
|
||||||
this.artistId, {
|
this.artistId, {
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final logger = getLogger(ArtistAlbumList);
|
final logger = getLogger(ArtistAlbumList);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final albumsQuery = useQueries.artist.albumsOf(ref, artistId);
|
final albumsQuery = ref.watch(artistAlbumsProvider(artistId));
|
||||||
|
final albumsQueryNotifier =
|
||||||
|
ref.watch(artistAlbumsProvider(artistId).notifier);
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
final albums = albumsQuery.asData?.value.items ?? [];
|
||||||
return albumsQuery.pages
|
|
||||||
.expand<Album>((page) => page.items ?? const Iterable.empty())
|
|
||||||
.toList();
|
|
||||||
}, [albumsQuery.pages]);
|
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<Album>(
|
return HorizontalPlaybuttonCardView<Album>(
|
||||||
isLoadingNextPage: albumsQuery.isLoadingNextPage,
|
isLoadingNextPage: albumsQuery.isLoadingNextPage,
|
||||||
hasNextPage: albumsQuery.hasNextPage,
|
hasNextPage: albumsQuery.asData?.value.hasMore ?? false,
|
||||||
items: albums,
|
items: albums,
|
||||||
onFetchMore: albumsQuery.fetchNext,
|
onFetchMore: albumsQueryNotifier.fetchMore,
|
||||||
title: Text(
|
title: Text(
|
||||||
context.l10n.albums,
|
context.l10n.albums,
|
||||||
style: theme.textTheme.headlineSmall,
|
style: theme.textTheme.headlineSmall,
|
||||||
|
|||||||
@ -6,27 +6,26 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/provider/blacklist_provider.dart';
|
import 'package:spotube/provider/blacklist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class ArtistCard extends HookConsumerWidget {
|
class ArtistCard extends HookConsumerWidget {
|
||||||
final Artist artist;
|
final Artist artist;
|
||||||
const ArtistCard(this.artist, {Key? key}) : super(key: key);
|
const ArtistCard(this.artist, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final backgroundImage = UniversalImage.imageProvider(
|
final backgroundImage = UniversalImage.imageProvider(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
artist.images.asUrlString(
|
||||||
artist.images,
|
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final isBlackListed = ref.watch(
|
final isBlackListed = ref.watch(
|
||||||
BlackListNotifier.provider.select(
|
blacklistProvider.select(
|
||||||
(blacklist) => blacklist.contains(
|
(blacklist) => blacklist.contains(
|
||||||
BlacklistedElement.artist(artist.id!, artist.name!),
|
BlacklistedElement.artist(artist.id!, artist.name!),
|
||||||
),
|
),
|
||||||
|
|||||||
122
lib/components/connect/connect_device.dart
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class ConnectDeviceButton extends HookConsumerWidget {
|
||||||
|
final bool _sidebar;
|
||||||
|
const ConnectDeviceButton({super.key}) : _sidebar = false;
|
||||||
|
const ConnectDeviceButton.sidebar({super.key}) : _sidebar = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final ThemeData(:colorScheme) = Theme.of(context);
|
||||||
|
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
|
||||||
|
if (_sidebar) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ServiceUtils.push(context, "/connect");
|
||||||
|
},
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(5),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.devices),
|
||||||
|
if (connectClients.asData?.value.services.isNotEmpty == true)
|
||||||
|
Text(
|
||||||
|
" (${connectClients.asData?.value.services.length})",
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Icon(SpotubeIcons.speaker),
|
||||||
|
const Gap(5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 40 * pixelRatio,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
fit: StackFit.loose,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Center(
|
||||||
|
child: ClipRect(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
ServiceUtils.push(context, "/connect");
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: Ink(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
color: colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (connectClients.asData?.value.resolvedService !=
|
||||||
|
null) ...[
|
||||||
|
Container(
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.greenAccent,
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(5),
|
||||||
|
],
|
||||||
|
Text(context.l10n.devices),
|
||||||
|
if (connectClients.asData?.value.services.isNotEmpty ==
|
||||||
|
true)
|
||||||
|
Text(
|
||||||
|
" (${connectClients.asData?.value.services.length})",
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.onPrimaryContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(35),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: -3,
|
||||||
|
child: IconButton.filled(
|
||||||
|
icon: const Icon(SpotubeIcons.speaker),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
foregroundColor: colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
ServiceUtils.push(context, "/connect");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
lib/components/connect/local_devices.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
|
||||||
|
class ConnectPageLocalDevices extends HookWidget {
|
||||||
|
const ConnectPageLocalDevices({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData(:textTheme) = Theme.of(context);
|
||||||
|
final devicesFuture = useFuture(audioPlayer.devices);
|
||||||
|
final devicesStream = useStream(audioPlayer.devicesStream);
|
||||||
|
final selectedDeviceFuture = useFuture(audioPlayer.selectedDevice);
|
||||||
|
final selectedDeviceStream = useStream(audioPlayer.selectedDeviceStream);
|
||||||
|
|
||||||
|
final devices = devicesStream.data ?? devicesFuture.data;
|
||||||
|
final selectedDevice =
|
||||||
|
selectedDeviceStream.data ?? selectedDeviceFuture.data;
|
||||||
|
|
||||||
|
if (devices == null) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
const SliverGap(10),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: Text(
|
||||||
|
context.l10n.this_device,
|
||||||
|
style: textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverGap(10),
|
||||||
|
SliverList.separated(
|
||||||
|
itemCount: devices.length,
|
||||||
|
separatorBuilder: (context, index) => const Gap(10),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final device = devices[index];
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(SpotubeIcons.speaker),
|
||||||
|
title: Text(device.description),
|
||||||
|
subtitle: Text(device.name),
|
||||||
|
selected: selectedDevice == device,
|
||||||
|
onTap: () => audioPlayer.setAudioDevice(device),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,14 +8,13 @@ import 'package:spotube/provider/authentication_provider.dart';
|
|||||||
class TokenLoginForm extends HookConsumerWidget {
|
class TokenLoginForm extends HookConsumerWidget {
|
||||||
final void Function()? onDone;
|
final void Function()? onDone;
|
||||||
const TokenLoginForm({
|
const TokenLoginForm({
|
||||||
Key? key,
|
super.key,
|
||||||
this.onDone,
|
this.onDone,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final authenticationNotifier =
|
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
|
||||||
ref.watch(AuthenticationNotifier.provider.notifier);
|
|
||||||
final directCodeController = useTextEditingController();
|
final directCodeController = useTextEditingController();
|
||||||
final mounted = useIsMounted();
|
final mounted = useIsMounted();
|
||||||
|
|
||||||
|
|||||||
31
lib/components/getting_started/blur_card.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class BlurCard extends HookConsumerWidget {
|
||||||
|
final Widget child;
|
||||||
|
const BlurCard({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,35 +1,28 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomeFeaturedSection extends HookConsumerWidget {
|
class HomeFeaturedSection extends HookConsumerWidget {
|
||||||
const HomeFeaturedSection({Key? key}) : super(key: key);
|
const HomeFeaturedSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final featuredPlaylistsQuery = useQueries.playlist.featured(ref);
|
final featuredPlaylists = ref.watch(featuredPlaylistsProvider);
|
||||||
final playlists = useMemoized(
|
final featuredPlaylistsNotifier =
|
||||||
() => featuredPlaylistsQuery.pages
|
ref.watch(featuredPlaylistsProvider.notifier);
|
||||||
.whereType<Page<PlaylistSimple>>()
|
|
||||||
.expand((page) => page.items ?? const <PlaylistSimple>[]),
|
|
||||||
[featuredPlaylistsQuery.pages],
|
|
||||||
);
|
|
||||||
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
|
|
||||||
!featuredPlaylistsQuery.isLoadingNextPage;
|
|
||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
enabled: isLoadingFeaturedPlaylists,
|
enabled: featuredPlaylists.isLoading,
|
||||||
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
|
||||||
items: playlists.toList(),
|
items: featuredPlaylists.asData?.value.items ?? [],
|
||||||
title: Text(context.l10n.featured),
|
title: Text(context.l10n.featured),
|
||||||
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage,
|
isLoadingNextPage: featuredPlaylists.isLoadingNextPage,
|
||||||
hasNextPage: featuredPlaylistsQuery.hasNextPage,
|
hasNextPage: featuredPlaylists.asData?.value.hasMore ?? false,
|
||||||
onFetchMore: featuredPlaylistsQuery.fetchNext,
|
onFetchMore: featuredPlaylistsNotifier.fetchMore,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
52
lib/components/home/sections/feed.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
|
import 'package:spotube/provider/spotify/views/home.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
|
class HomePageFeedSection extends HookConsumerWidget {
|
||||||
|
const HomePageFeedSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final homeFeed = ref.watch(homeViewProvider);
|
||||||
|
final nonShortSections = homeFeed.asData?.value?.sections
|
||||||
|
.where((s) => s.typename == "HomeGenericSectionData")
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: nonShortSections.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final section = nonShortSections[index];
|
||||||
|
if (section.items.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return HorizontalPlaybuttonCardView(
|
||||||
|
items: [
|
||||||
|
for (final item in section.items)
|
||||||
|
if (item.album != null)
|
||||||
|
item.album!.asAlbum
|
||||||
|
else if (item.artist != null)
|
||||||
|
item.artist!.asArtist
|
||||||
|
else if (item.playlist != null)
|
||||||
|
item.playlist!.asPlaylist
|
||||||
|
],
|
||||||
|
title: Text(section.title ?? "No Titel"),
|
||||||
|
hasNextPage: false,
|
||||||
|
isLoadingNextPage: false,
|
||||||
|
onFetchMore: () {},
|
||||||
|
titleTrailing: Directionality(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
child: TextButton.icon(
|
||||||
|
label: const Text("Browse More"),
|
||||||
|
icon: const Icon(SpotubeIcons.angleRight),
|
||||||
|
onPressed: () =>
|
||||||
|
ServiceUtils.push(context, "/feeds/${section.uri}"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
@ -5,15 +7,16 @@ import 'package:spotube/collections/fake.dart';
|
|||||||
import 'package:spotube/components/home/sections/friends/friend_item.dart';
|
import 'package:spotube/components/home/sections/friends/friend_item.dart';
|
||||||
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomePageFriendsSection extends HookConsumerWidget {
|
class HomePageFriendsSection extends HookConsumerWidget {
|
||||||
const HomePageFriendsSection({Key? key}) : super(key: key);
|
const HomePageFriendsSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final friendsQuery = useQueries.user.friendActivity(ref);
|
final friendsQuery = ref.watch(friendsProvider);
|
||||||
final friends = friendsQuery.data?.friends ?? FakeData.friends.friends;
|
final friends =
|
||||||
|
friendsQuery.asData?.value.friends ?? FakeData.friends.friends;
|
||||||
|
|
||||||
final groupCount = useBreakpointValue(
|
final groupCount = useBreakpointValue(
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@ -48,8 +51,8 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!friendsQuery.isLoading &&
|
if (friendsQuery.isLoading ||
|
||||||
(!friendsQuery.hasData || friendsQuery.data!.friends.isEmpty)) {
|
friendsQuery.asData?.value.friends.isEmpty == true) {
|
||||||
return const SliverToBoxAdapter(
|
return const SliverToBoxAdapter(
|
||||||
child: SizedBox.shrink(),
|
child: SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
@ -69,22 +72,27 @@ class HomePageFriendsSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: SingleChildScrollView(
|
child: ScrollConfiguration(
|
||||||
scrollDirection: Axis.horizontal,
|
behavior: ScrollConfiguration.of(context).copyWith(
|
||||||
child: Column(
|
dragDevices: PointerDeviceKind.values.toSet(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
for (final group in friendGroup)
|
scrollDirection: Axis.horizontal,
|
||||||
Row(
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
for (final friend in group)
|
children: [
|
||||||
Padding(
|
for (final group in friendGroup)
|
||||||
padding: const EdgeInsets.all(8.0),
|
Row(
|
||||||
child: FriendItem(friend: friend),
|
children: [
|
||||||
),
|
for (final friend in group)
|
||||||
],
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.all(8.0),
|
||||||
],
|
child: FriendItem(friend: friend),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/models/spotify_friends.dart';
|
import 'package:spotube/models/spotify_friends.dart';
|
||||||
@ -13,9 +11,9 @@ import 'package:spotube/provider/spotify_provider.dart';
|
|||||||
class FriendItem extends HookConsumerWidget {
|
class FriendItem extends HookConsumerWidget {
|
||||||
final SpotifyFriendActivity friend;
|
final SpotifyFriendActivity friend;
|
||||||
const FriendItem({
|
const FriendItem({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.friend,
|
required this.friend,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -24,7 +22,6 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
) = Theme.of(context);
|
) = Theme.of(context);
|
||||||
|
|
||||||
final queryClient = useQueryClient();
|
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@ -86,15 +83,11 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
..onTap = () async {
|
..onTap = () async {
|
||||||
context.push(
|
context.push(
|
||||||
"/${friend.track.context.path}",
|
"/${friend.track.context.path}",
|
||||||
extra: !friend.track.context.path
|
extra:
|
||||||
.startsWith("album")
|
!friend.track.context.path.startsWith("album")
|
||||||
? null
|
? null
|
||||||
: await queryClient.fetchQuery<Album, dynamic>(
|
: await spotify.albums
|
||||||
"album/${friend.track.album.id}",
|
.get(friend.track.context.id),
|
||||||
() => spotify.albums.get(
|
|
||||||
friend.track.album.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -110,12 +103,7 @@ class FriendItem extends HookConsumerWidget {
|
|||||||
recognizer: TapGestureRecognizer()
|
recognizer: TapGestureRecognizer()
|
||||||
..onTap = () async {
|
..onTap = () async {
|
||||||
final album =
|
final album =
|
||||||
await queryClient.fetchQuery<Album, dynamic>(
|
await spotify.albums.get(friend.track.album.id);
|
||||||
"album/${friend.track.album.id}",
|
|
||||||
() => spotify.albums.get(
|
|
||||||
friend.track.album.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.push(
|
context.push(
|
||||||
"/album/${friend.track.album.id}",
|
"/album/${friend.track.album.id}",
|
||||||
|
|||||||
@ -13,28 +13,26 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
|
|
||||||
class HomeGenresSection extends HookConsumerWidget {
|
class HomeGenresSection extends HookConsumerWidget {
|
||||||
const HomeGenresSection({Key? key}) : super(key: key);
|
const HomeGenresSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
final recommendationMarket = ref.watch(
|
final categoriesQuery = ref.watch(categoriesProvider);
|
||||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
final categories = useMemoized(
|
||||||
|
() =>
|
||||||
|
categoriesQuery.asData?.value
|
||||||
|
.where((c) => (c.icons?.length ?? 0) > 0)
|
||||||
|
.take(mediaQuery.mdAndDown ? 6 : 10)
|
||||||
|
.toList() ??
|
||||||
|
<Category>[],
|
||||||
|
[mediaQuery.mdAndDown, categoriesQuery.asData?.value],
|
||||||
);
|
);
|
||||||
final categoriesQuery =
|
|
||||||
useQueries.category.listAll(ref, recommendationMarket);
|
|
||||||
|
|
||||||
final categories = categoriesQuery.data
|
|
||||||
?.where((c) => (c.icons?.length ?? 0) > 0)
|
|
||||||
.take(mediaQuery.mdAndDown ? 6 : 10)
|
|
||||||
.toList() ??
|
|
||||||
<Category>[];
|
|
||||||
|
|
||||||
return SliverMainAxisGroup(
|
return SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|||||||
@ -2,19 +2,19 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class HomeMadeForUserSection extends HookConsumerWidget {
|
class HomeMadeForUserSection extends HookConsumerWidget {
|
||||||
const HomeMadeForUserSection({Key? key}) : super(key: key);
|
const HomeMadeForUserSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final madeForUser = useQueries.views.get(ref, "made-for-x-hub");
|
final madeForUser = ref.watch(viewProvider("made-for-x-hub"));
|
||||||
|
|
||||||
return SliverList.builder(
|
return SliverList.builder(
|
||||||
itemCount: madeForUser.data?["content"]?["items"]?.length ?? 0,
|
itemCount: madeForUser.asData?.value["content"]?["items"]?.length ?? 0,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = madeForUser.data?["content"]?["items"]?[index];
|
final item = madeForUser.asData?.value["content"]?["items"]?[index];
|
||||||
final playlists = item["content"]?["items"]
|
final playlists = item["content"]?["items"]
|
||||||
?.where((itemL2) => itemL2["type"] == "playlist")
|
?.where((itemL2) => itemL2["type"] == "playlist")
|
||||||
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
.map((itemL2) => PlaylistSimple.fromJson(itemL2))
|
||||||
|
|||||||
@ -1,56 +1,35 @@
|
|||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class HomeNewReleasesSection extends HookConsumerWidget {
|
class HomeNewReleasesSection extends HookConsumerWidget {
|
||||||
const HomeNewReleasesSection({Key? key}) : super(key: key);
|
const HomeNewReleasesSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
final newReleases = useQueries.album.newReleases(ref);
|
final newReleases = ref.watch(albumReleasesProvider);
|
||||||
final userArtistsQuery = useQueries.artist.followedByMeAll(ref);
|
final newReleasesNotifier = ref.read(albumReleasesProvider.notifier);
|
||||||
final userArtists =
|
|
||||||
userArtistsQuery.data?.map((s) => s.id!).toList() ?? const [];
|
|
||||||
|
|
||||||
final albums = useMemoized(
|
final albums = ref.watch(userArtistAlbumReleasesProvider);
|
||||||
() {
|
|
||||||
final allReleases = newReleases.pages
|
|
||||||
.whereType<Page<AlbumSimple>>()
|
|
||||||
.expand((page) => page.items ?? const <AlbumSimple>[])
|
|
||||||
.map((album) => TypeConversionUtils.simpleAlbum_X_Album(album));
|
|
||||||
|
|
||||||
final userArtistReleases = allReleases.where((album) {
|
if (auth == null ||
|
||||||
return album.artists
|
newReleases.isLoading ||
|
||||||
?.any((artist) => userArtists.contains(artist.id!)) ==
|
newReleases.asData?.value.items.isEmpty == true) {
|
||||||
true;
|
return const SizedBox.shrink();
|
||||||
}).toList();
|
}
|
||||||
|
|
||||||
if (userArtistReleases.isEmpty) return allReleases.toList();
|
|
||||||
return userArtistReleases;
|
|
||||||
},
|
|
||||||
[newReleases.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
final hasNewReleases = newReleases.hasPageData &&
|
|
||||||
userArtistsQuery.hasData &&
|
|
||||||
!newReleases.isLoadingNextPage;
|
|
||||||
|
|
||||||
if (auth == null || !hasNewReleases) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return HorizontalPlaybuttonCardView<Album>(
|
return HorizontalPlaybuttonCardView<Album>(
|
||||||
items: albums,
|
items: albums,
|
||||||
title: Text(context.l10n.new_releases),
|
title: Text(context.l10n.new_releases),
|
||||||
isLoadingNextPage: newReleases.isLoadingNextPage,
|
isLoadingNextPage: newReleases.isLoadingNextPage,
|
||||||
hasNextPage: newReleases.hasNextPage,
|
hasNextPage: newReleases.asData?.value.hasMore ?? false,
|
||||||
onFetchMore: newReleases.fetchNext,
|
onFetchMore: newReleasesNotifier.fetchMore,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class MultiSelectField<T> extends HookWidget {
|
|||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
const MultiSelectField({
|
const MultiSelectField({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.selectedOptions,
|
required this.selectedOptions,
|
||||||
required this.getValueForOption,
|
required this.getValueForOption,
|
||||||
@ -36,7 +36,7 @@ class MultiSelectField<T> extends HookWidget {
|
|||||||
this.dialogTitle,
|
this.dialogTitle,
|
||||||
this.helperText,
|
this.helperText,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
Widget defaultSelectedOptionBuilder(T option) {
|
Widget defaultSelectedOptionBuilder(T option) {
|
||||||
return Chip(
|
return Chip(
|
||||||
@ -134,14 +134,14 @@ class _MultiSelectDialog<T> extends HookWidget {
|
|||||||
final String? helperText;
|
final String? helperText;
|
||||||
|
|
||||||
const _MultiSelectDialog({
|
const _MultiSelectDialog({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.dialogTitle,
|
required this.dialogTitle,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.getValueForOption,
|
required this.getValueForOption,
|
||||||
this.optionBuilder,
|
this.optionBuilder,
|
||||||
this.initialSelection = const [],
|
this.initialSelection = const [],
|
||||||
this.helperText,
|
this.helperText,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -20,12 +20,12 @@ class RecommendationAttributeDials extends HookWidget {
|
|||||||
final double base;
|
final double base;
|
||||||
|
|
||||||
const RecommendationAttributeDials({
|
const RecommendationAttributeDials({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.values,
|
required this.values,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.base = 1,
|
this.base = 1,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -12,12 +12,12 @@ class RecommendationAttributeFields extends HookWidget {
|
|||||||
final Map<String, RecommendationAttribute>? presets;
|
final Map<String, RecommendationAttribute>? presets;
|
||||||
|
|
||||||
const RecommendationAttributeFields({
|
const RecommendationAttributeFields({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.values,
|
required this.values,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.presets,
|
this.presets,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
|
|||||||
final SelectedItemDisplayType selectedItemDisplayType;
|
final SelectedItemDisplayType selectedItemDisplayType;
|
||||||
|
|
||||||
const SeedsMultiAutocomplete({
|
const SeedsMultiAutocomplete({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.seeds,
|
required this.seeds,
|
||||||
required this.fetchSeeds,
|
required this.fetchSeeds,
|
||||||
required this.autocompleteOptionBuilder,
|
required this.autocompleteOptionBuilder,
|
||||||
@ -35,7 +35,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
|
|||||||
this.inputDecoration,
|
this.inputDecoration,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
|
this.selectedItemDisplayType = SelectedItemDisplayType.wrap,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
|
||||||
class SimpleTrackTile extends HookWidget {
|
class SimpleTrackTile extends HookWidget {
|
||||||
final Track track;
|
final Track track;
|
||||||
final VoidCallback? onDelete;
|
final VoidCallback? onDelete;
|
||||||
const SimpleTrackTile({
|
const SimpleTrackTile({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -21,8 +21,7 @@ class SimpleTrackTile extends HookWidget {
|
|||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: TypeConversionUtils.image_X_UrlString(
|
path: (track.album?.images).asUrlString(
|
||||||
track.album?.images,
|
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
),
|
),
|
||||||
height: 40,
|
height: 40,
|
||||||
|
|||||||
@ -2,123 +2,113 @@ import 'package:flutter/material.dart' hide Image;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/album/album_card.dart';
|
import 'package:spotube/components/album/album_card.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/components/shared/waypoint.dart';
|
import 'package:spotube/components/shared/waypoint.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class UserAlbums extends HookConsumerWidget {
|
class UserAlbums extends HookConsumerWidget {
|
||||||
const UserAlbums({Key? key}) : super(key: key);
|
const UserAlbums({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final albumsQuery = useQueries.album.ofMine(ref);
|
final albumsQuery = ref.watch(favoriteAlbumsProvider);
|
||||||
|
final albumsQueryNotifier = ref.watch(favoriteAlbumsProvider.notifier);
|
||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
|
|
||||||
final allAlbums = useMemoized(
|
|
||||||
() => albumsQuery.pages
|
|
||||||
.expand((element) => element.items ?? <AlbumSimple>[]),
|
|
||||||
[albumsQuery.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
final albums = useMemoized(() {
|
final albums = useMemoized(() {
|
||||||
if (searchText.value.isEmpty) {
|
if (searchText.value.isEmpty) {
|
||||||
return allAlbums;
|
return albumsQuery.asData?.value.items ?? [];
|
||||||
}
|
}
|
||||||
return allAlbums
|
return albumsQuery.asData?.value.items
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(e.name!, searchText.value),
|
weightedRatio(e.name!, searchText.value),
|
||||||
e,
|
e,
|
||||||
))
|
))
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
.where((e) => e.$1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
.toList();
|
.toList() ??
|
||||||
}, [allAlbums, searchText.value]);
|
[];
|
||||||
|
}, [albumsQuery.asData?.value, searchText.value]);
|
||||||
|
|
||||||
if (auth == null) {
|
if (auth == null) {
|
||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
final theme = Theme.of(context);
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
return RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await albumsQuery.refresh();
|
ref.invalidate(favoriteAlbumsProvider);
|
||||||
},
|
},
|
||||||
child: SafeArea(
|
child: InterScrollbar(
|
||||||
child: Scaffold(
|
controller: controller,
|
||||||
appBar: PreferredSize(
|
child: CustomScrollView(
|
||||||
preferredSize: const Size.fromHeight(50),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: ColoredBox(
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
child: SearchBar(
|
|
||||||
onChanged: (value) => searchText.value = value,
|
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
|
||||||
hintText: context.l10n.filter_albums,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SizedBox.expand(
|
|
||||||
child: InterScrollbar(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: SingleChildScrollView(
|
slivers: [
|
||||||
padding: const EdgeInsets.all(8.0),
|
SliverAppBar(
|
||||||
controller: controller,
|
floating: true,
|
||||||
child: Skeletonizer(
|
flexibleSpace: Padding(
|
||||||
enabled: albumsQuery.pages.isEmpty,
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: Center(
|
child: SearchBar(
|
||||||
child: Wrap(
|
onChanged: (value) => searchText.value = value,
|
||||||
runSpacing: 20,
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
alignment: WrapAlignment.center,
|
hintText: context.l10n.filter_albums,
|
||||||
runAlignment: WrapAlignment.center,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (albumsQuery.pages.isEmpty)
|
|
||||||
...List.generate(
|
|
||||||
10,
|
|
||||||
(index) => AlbumCard(FakeData.album),
|
|
||||||
)
|
|
||||||
else if (albums.isEmpty)
|
|
||||||
const Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [NotFound()],
|
|
||||||
),
|
|
||||||
for (final album in albums)
|
|
||||||
AlbumCard(
|
|
||||||
TypeConversionUtils.simpleAlbum_X_Album(album),
|
|
||||||
),
|
|
||||||
if (albums.isNotEmpty && albumsQuery.hasNextPage)
|
|
||||||
Waypoint(
|
|
||||||
controller: controller,
|
|
||||||
isGrid: true,
|
|
||||||
onTouchEdge: albumsQuery.fetchNext,
|
|
||||||
child: AlbumCard(FakeData.album),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SliverGap(10),
|
||||||
|
Skeletonizer.sliver(
|
||||||
|
enabled: albumsQuery.isLoading,
|
||||||
|
child: SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
|
return SliverGrid.builder(
|
||||||
|
itemCount: albums.isEmpty ? 6 : albums.length + 1,
|
||||||
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (albums.isNotEmpty && index == albums.length) {
|
||||||
|
if (albumsQuery.asData?.value.hasMore != true) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Waypoint(
|
||||||
|
controller: controller,
|
||||||
|
isGrid: true,
|
||||||
|
onTouchEdge: albumsQueryNotifier.fetchMore,
|
||||||
|
child: Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: AlbumCard(FakeData.albumSimple),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlbumCard(
|
||||||
|
albums.elementAtOrNull(index) ?? FakeData.albumSimple,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
@ -9,26 +10,27 @@ import 'package:spotube/collections/fake.dart';
|
|||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
|
||||||
import 'package:spotube/components/artist/artist_card.dart';
|
import 'package:spotube/components/artist/artist_card.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||||
|
import 'package:spotube/components/shared/waypoint.dart';
|
||||||
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
class UserArtists extends HookConsumerWidget {
|
class UserArtists extends HookConsumerWidget {
|
||||||
const UserArtists({Key? key}) : super(key: key);
|
const UserArtists({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
|
||||||
|
|
||||||
final artistQuery = useQueries.artist.followedByMeAll(ref);
|
final artistQuery = ref.watch(followedArtistsProvider);
|
||||||
|
final artistQueryNotifier = ref.watch(followedArtistsProvider.notifier);
|
||||||
|
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
|
|
||||||
final filteredArtists = useMemoized(() {
|
final filteredArtists = useMemoized(() {
|
||||||
final artists = artistQuery.data ?? [];
|
final artists = artistQuery.asData?.value.items ?? [];
|
||||||
|
|
||||||
if (searchText.value.isEmpty) {
|
if (searchText.value.isEmpty) {
|
||||||
return artists.toList();
|
return artists.toList();
|
||||||
@ -42,7 +44,7 @@ class UserArtists extends HookConsumerWidget {
|
|||||||
.where((e) => e.$1 > 50)
|
.where((e) => e.$1 > 50)
|
||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
.toList();
|
.toList();
|
||||||
}, [artistQuery.data, searchText.value]);
|
}, [artistQuery.asData?.value.items, searchText.value]);
|
||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
|
|
||||||
@ -50,76 +52,73 @@ class UserArtists extends HookConsumerWidget {
|
|||||||
return const AnonymousFallback();
|
return const AnonymousFallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return SafeArea(
|
||||||
appBar: PreferredSize(
|
child: Scaffold(
|
||||||
preferredSize: const Size.fromHeight(50),
|
body: RefreshIndicator(
|
||||||
child: Padding(
|
onRefresh: () async {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
ref.invalidate(followedArtistsProvider);
|
||||||
child: ColoredBox(
|
},
|
||||||
color: theme.scaffoldBackgroundColor,
|
child: InterScrollbar(
|
||||||
child: SearchBar(
|
controller: controller,
|
||||||
onChanged: (value) => searchText.value = value,
|
child: Padding(
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
hintText: context.l10n.filter_artist,
|
child: CustomScrollView(
|
||||||
|
controller: controller,
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
flexibleSpace: SearchBar(
|
||||||
|
onChanged: (value) => searchText.value = value,
|
||||||
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
|
hintText: context.l10n.filter_artist,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverGap(10),
|
||||||
|
Skeletonizer.sliver(
|
||||||
|
enabled: artistQuery.isLoading,
|
||||||
|
child: SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
|
return SliverGrid.builder(
|
||||||
|
itemCount: filteredArtists.isEmpty
|
||||||
|
? 6
|
||||||
|
: filteredArtists.length + 1,
|
||||||
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 200,
|
||||||
|
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (filteredArtists.isNotEmpty &&
|
||||||
|
index == filteredArtists.length) {
|
||||||
|
if (artistQuery.asData?.value.hasMore != true) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Waypoint(
|
||||||
|
controller: controller,
|
||||||
|
isGrid: true,
|
||||||
|
onTouchEdge: artistQueryNotifier.fetchMore,
|
||||||
|
child: Skeletonizer(
|
||||||
|
enabled: true,
|
||||||
|
child: ArtistCard(FakeData.artist),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ArtistCard(
|
||||||
|
filteredArtists.elementAtOrNull(index) ??
|
||||||
|
FakeData.artist,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
|
||||||
body: artistQuery.data?.isEmpty == true
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Text(context.l10n.loading),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await artistQuery.refresh();
|
|
||||||
},
|
|
||||||
child: InterScrollbar(
|
|
||||||
controller: controller,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: controller,
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: SafeArea(
|
|
||||||
child: Center(
|
|
||||||
child: Skeletonizer(
|
|
||||||
enabled: artistQuery.isLoading,
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 15,
|
|
||||||
runSpacing: 5,
|
|
||||||
children: artistQuery.isLoading
|
|
||||||
? List.generate(
|
|
||||||
10, (index) => ArtistCard(FakeData.artist))
|
|
||||||
: filteredArtists.isEmpty
|
|
||||||
? [
|
|
||||||
const Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
NotFound(),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
: filteredArtists
|
|
||||||
.mapIndexed((index, artist) =>
|
|
||||||
ArtistCard(artist))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
|
|
||||||
class UserDownloads extends HookConsumerWidget {
|
class UserDownloads extends HookConsumerWidget {
|
||||||
const UserDownloads({Key? key}) : super(key: key);
|
const UserDownloads({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
|||||||
@ -4,18 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/components/shared/links/artist_link.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/services/download_manager/download_status.dart';
|
import 'package:spotube/services/download_manager/download_status.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class DownloadItem extends HookConsumerWidget {
|
class DownloadItem extends HookConsumerWidget {
|
||||||
final Track track;
|
final Track track;
|
||||||
const DownloadItem({
|
const DownloadItem({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.track,
|
required this.track,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -51,16 +52,15 @@ class DownloadItem extends HookConsumerWidget {
|
|||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
path: TypeConversionUtils.image_X_UrlString(
|
path: (track.album?.images).asUrlString(
|
||||||
track.album?.images,
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(track.name ?? ''),
|
title: Text(track.name ?? ''),
|
||||||
subtitle: TypeConversionUtils.artists_X_ClickableArtists(
|
subtitle: ArtistLink(
|
||||||
track.artists ?? <Artist>[],
|
artists: track.artists ?? <Artist>[],
|
||||||
mainAxisAlignment: WrapAlignment.start,
|
mainAxisAlignment: WrapAlignment.start,
|
||||||
),
|
),
|
||||||
trailing: isQueryingSourceInfo
|
trailing: isQueryingSourceInfo
|
||||||
|
|||||||
@ -21,12 +21,14 @@ import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
|||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
|
||||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart' show FfiException;
|
||||||
|
|
||||||
const supportedAudioTypes = [
|
const supportedAudioTypes = [
|
||||||
@ -50,10 +52,11 @@ enum SortBy {
|
|||||||
none,
|
none,
|
||||||
ascending,
|
ascending,
|
||||||
descending,
|
descending,
|
||||||
artist,
|
|
||||||
album,
|
|
||||||
newest,
|
newest,
|
||||||
oldest,
|
oldest,
|
||||||
|
duration,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
}
|
}
|
||||||
|
|
||||||
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
||||||
@ -110,7 +113,7 @@ final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
|||||||
final tracks = filesWithMetadata
|
final tracks = filesWithMetadata
|
||||||
.map(
|
.map(
|
||||||
(fileWithMetadata) => LocalTrack.fromTrack(
|
(fileWithMetadata) => LocalTrack.fromTrack(
|
||||||
track: TypeConversionUtils.localTrack_X_Track(
|
track: Track().fromFile(
|
||||||
fileWithMetadata["file"],
|
fileWithMetadata["file"],
|
||||||
metadata: fileWithMetadata["metadata"],
|
metadata: fileWithMetadata["metadata"],
|
||||||
art: fileWithMetadata["art"],
|
art: fileWithMetadata["art"],
|
||||||
@ -128,15 +131,15 @@ final localTracksProvider = FutureProvider<List<LocalTrack>>((ref) async {
|
|||||||
});
|
});
|
||||||
|
|
||||||
class UserLocalTracks extends HookConsumerWidget {
|
class UserLocalTracks extends HookConsumerWidget {
|
||||||
const UserLocalTracks({Key? key}) : super(key: key);
|
const UserLocalTracks({super.key});
|
||||||
|
|
||||||
Future<void> playLocalTracks(
|
Future<void> playLocalTracks(
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
List<LocalTrack> tracks, {
|
List<LocalTrack> tracks, {
|
||||||
LocalTrack? currentTrack,
|
LocalTrack? currentTrack,
|
||||||
}) async {
|
}) async {
|
||||||
final playlist = ref.read(ProxyPlaylistNotifier.provider);
|
final playlist = ref.read(proxyPlaylistProvider);
|
||||||
final playback = ref.read(ProxyPlaylistNotifier.notifier);
|
final playback = ref.read(proxyPlaylistProvider.notifier);
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
final isPlaylistPlaying = playlist.containsTracks(tracks);
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
@ -155,10 +158,10 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final sortBy = useState<SortBy>(SortBy.none);
|
final sortBy = useState<SortBy>(SortBy.none);
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final trackSnapshot = ref.watch(localTracksProvider);
|
final trackSnapshot = ref.watch(localTracksProvider);
|
||||||
final isPlaylistPlaying =
|
final isPlaylistPlaying =
|
||||||
playlist.containsTracks(trackSnapshot.value ?? []);
|
playlist.containsTracks(trackSnapshot.asData?.value ?? []);
|
||||||
|
|
||||||
final searchController = useTextEditingController();
|
final searchController = useTextEditingController();
|
||||||
useValueListenable(searchController);
|
useValueListenable(searchController);
|
||||||
@ -173,19 +176,16 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 5),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: trackSnapshot.value != null
|
onPressed: trackSnapshot.asData?.value != null
|
||||||
? () async {
|
? () async {
|
||||||
if (trackSnapshot.value?.isNotEmpty == true) {
|
if (trackSnapshot.asData?.value.isNotEmpty == true) {
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
await playLocalTracks(
|
await playLocalTracks(
|
||||||
ref,
|
ref,
|
||||||
trackSnapshot.value!,
|
trackSnapshot.asData!.value,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// TODO: Remove stop capability
|
|
||||||
// playlistNotifier.stop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,11 +212,11 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
sortBy.value = value;
|
sortBy.value = value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 5),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
child: const Icon(SpotubeIcons.refresh),
|
child: const Icon(SpotubeIcons.refresh),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.refresh(localTracksProvider);
|
ref.invalidate(localTracksProvider);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -241,7 +241,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
return sortedTracks
|
return sortedTracks
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(
|
weightedRatio(
|
||||||
"${e.name} - ${TypeConversionUtils.artists_X_String<Artist>(e.artists ?? [])}",
|
"${e.name} - ${e.artists?.asString() ?? ""}",
|
||||||
searchController.text,
|
searchController.text,
|
||||||
),
|
),
|
||||||
e,
|
e,
|
||||||
@ -268,7 +268,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
return Expanded(
|
return Expanded(
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.refresh(localTracksProvider);
|
ref.invalidate(localTracksProvider);
|
||||||
},
|
},
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@ -281,12 +281,17 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
trackSnapshot.isLoading ? 5 : filteredTracks.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (trackSnapshot.isLoading) {
|
if (trackSnapshot.isLoading) {
|
||||||
return TrackTile(track: FakeData.track, index: index);
|
return TrackTile(
|
||||||
|
playlist: playlist,
|
||||||
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final track = filteredTracks[index];
|
final track = filteredTracks[index];
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
index: index,
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
track: track,
|
track: track,
|
||||||
userPlaylist: false,
|
userPlaylist: false,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@ -309,8 +314,11 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => TrackTile(
|
||||||
TrackTile(track: FakeData.track, index: index),
|
track: FakeData.track,
|
||||||
|
index: index,
|
||||||
|
playlist: playlist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart' hide Image;
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
@ -17,24 +18,21 @@ import 'package:spotube/components/shared/waypoint.dart';
|
|||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class UserPlaylists extends HookConsumerWidget {
|
class UserPlaylists extends HookConsumerWidget {
|
||||||
const UserPlaylists({Key? key}) : super(key: key);
|
const UserPlaylists({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
final playlistsQuery = useQueries.playlist.ofMine(ref);
|
final playlistsQuery = ref.watch(favoritePlaylistsProvider);
|
||||||
|
final playlistsQueryNotifier =
|
||||||
final pagePlaylists = useMemoized(
|
ref.watch(favoritePlaylistsProvider.notifier);
|
||||||
() => playlistsQuery.pages
|
|
||||||
.expand((page) => page.items?.toList() ?? <PlaylistSimple>[]),
|
|
||||||
[playlistsQuery.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
final likedTracksPlaylist = useMemoized(
|
final likedTracksPlaylist = useMemoized(
|
||||||
() => PlaylistSimple()
|
() => PlaylistSimple()
|
||||||
@ -58,12 +56,12 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
if (searchText.value.isEmpty) {
|
if (searchText.value.isEmpty) {
|
||||||
return [
|
return [
|
||||||
likedTracksPlaylist,
|
likedTracksPlaylist,
|
||||||
...pagePlaylists,
|
...?playlistsQuery.asData?.value.items,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
likedTracksPlaylist,
|
likedTracksPlaylist,
|
||||||
...pagePlaylists,
|
...?playlistsQuery.asData?.value.items,
|
||||||
]
|
]
|
||||||
.map((e) => (weightedRatio(e.name!, searchText.value), e))
|
.map((e) => (weightedRatio(e.name!, searchText.value), e))
|
||||||
.sorted((a, b) => b.$1.compareTo(a.$1))
|
.sorted((a, b) => b.$1.compareTo(a.$1))
|
||||||
@ -71,7 +69,7 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
.map((e) => e.$2)
|
.map((e) => e.$2)
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
[pagePlaylists, searchText.value],
|
[playlistsQuery, searchText.value],
|
||||||
);
|
);
|
||||||
|
|
||||||
final controller = useScrollController();
|
final controller = useScrollController();
|
||||||
@ -81,46 +79,46 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: playlistsQuery.refresh,
|
onRefresh: () async {
|
||||||
|
ref.invalidate(favoritePlaylistsProvider);
|
||||||
|
},
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: InterScrollbar(
|
child: InterScrollbar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
SliverAppBar(
|
||||||
child: Column(
|
floating: true,
|
||||||
mainAxisSize: MainAxisSize.min,
|
flexibleSpace: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
Padding(
|
child: SearchBar(
|
||||||
padding: const EdgeInsets.all(10),
|
onChanged: (value) => searchText.value = value,
|
||||||
child: SearchBar(
|
hintText: context.l10n.filter_playlists,
|
||||||
onChanged: (value) => searchText.value = value,
|
leading: const Icon(SpotubeIcons.filter),
|
||||||
hintText: context.l10n.filter_playlists,
|
),
|
||||||
leading: const Icon(SpotubeIcons.filter),
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize:
|
||||||
|
Size.fromHeight(kIsDesktop ? 35 : kToolbarHeight),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Gap(10),
|
||||||
|
const PlaylistCreateDialogButton(),
|
||||||
|
const Gap(10),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(SpotubeIcons.magic),
|
||||||
|
label: Text(context.l10n.generate_playlist),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).push("/library/generate");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
const Gap(10),
|
||||||
Row(
|
],
|
||||||
children: [
|
),
|
||||||
const SizedBox(width: 10),
|
|
||||||
const PlaylistCreateDialogButton(),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(SpotubeIcons.magic),
|
|
||||||
label: Text(context.l10n.generate_playlist),
|
|
||||||
onPressed: () {
|
|
||||||
GoRouter.of(context).push("/library/generate");
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverToBoxAdapter(
|
const SliverGap(10),
|
||||||
child: SizedBox(height: 10),
|
|
||||||
),
|
|
||||||
SliverLayoutBuilder(builder: (context, constrains) {
|
SliverLayoutBuilder(builder: (context, constrains) {
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
itemCount: playlists.isEmpty ? 6 : playlists.length + 1,
|
itemCount: playlists.isEmpty ? 6 : playlists.length + 1,
|
||||||
@ -132,14 +130,14 @@ class UserPlaylists extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (playlists.isNotEmpty && index == playlists.length) {
|
if (playlists.isNotEmpty && index == playlists.length) {
|
||||||
if (!playlistsQuery.hasNextPage) {
|
if (playlistsQuery.asData?.value.hasMore != true) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Waypoint(
|
return Waypoint(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isGrid: true,
|
isGrid: true,
|
||||||
onTouchEdge: playlistsQuery.fetchNext,
|
onTouchEdge: playlistsQueryNotifier.fetchMore,
|
||||||
child: Skeletonizer(
|
child: Skeletonizer(
|
||||||
enabled: true,
|
enabled: true,
|
||||||
child: PlaylistCard(FakeData.playlistSimple),
|
child: PlaylistCard(FakeData.playlistSimple),
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class ZoomControls extends HookWidget {
|
|||||||
final String unit;
|
final String unit;
|
||||||
|
|
||||||
const ZoomControls({
|
const ZoomControls({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
this.min,
|
this.min,
|
||||||
@ -27,7 +27,7 @@ class ZoomControls extends HookWidget {
|
|||||||
this.decreaseIcon = const Icon(SpotubeIcons.zoomOut),
|
this.decreaseIcon = const Icon(SpotubeIcons.zoomOut),
|
||||||
this.direction = Axis.horizontal,
|
this.direction = Axis.horizontal,
|
||||||
this.unit = "%",
|
this.unit = "%",
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart' hide Offset;
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/player/player_actions.dart';
|
import 'package:spotube/components/player/player_actions.dart';
|
||||||
@ -13,38 +12,44 @@ import 'package:spotube/components/player/player_queue.dart';
|
|||||||
import 'package:spotube/components/player/volume_slider.dart';
|
import 'package:spotube/components/player/volume_slider.dart';
|
||||||
import 'package:spotube/components/shared/animated_gradient.dart';
|
import 'package:spotube/components/shared/animated_gradient.dart';
|
||||||
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
|
import 'package:spotube/components/shared/dialogs/track_details_dialog.dart';
|
||||||
|
import 'package:spotube/components/shared/links/artist_link.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/components/shared/panels/sliding_up_panel.dart';
|
import 'package:spotube/components/shared/panels/sliding_up_panel.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
||||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/pages/lyrics/lyrics.dart';
|
import 'package:spotube/pages/lyrics/lyrics.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
|
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class PlayerView extends HookConsumerWidget {
|
class PlayerView extends HookConsumerWidget {
|
||||||
final PanelController panelController;
|
final PanelController panelController;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
const PlayerView({
|
const PlayerView({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.panelController,
|
required this.panelController,
|
||||||
required this.scrollController,
|
required this.scrollController,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final currentTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
|
final sourcedCurrentTrack = ref.watch(activeSourcedTrackProvider);
|
||||||
(value) => value.activeTrack,
|
final currentActiveTrack =
|
||||||
));
|
ref.watch(proxyPlaylistProvider.select((s) => s.activeTrack));
|
||||||
final isLocalTrack = ref.watch(ProxyPlaylistNotifier.provider.select(
|
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
|
||||||
(value) => value.activeTrack is LocalTrack,
|
final isLocalTrack = currentTrack is LocalTrack;
|
||||||
));
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
@ -57,8 +62,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
}, [mediaQuery.lgAndUp]);
|
}, [mediaQuery.lgAndUp]);
|
||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => TypeConversionUtils.image_X_UrlString(
|
() => (currentTrack?.album?.images).asUrlString(
|
||||||
currentTrack?.album?.images,
|
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[currentTrack?.album?.images],
|
[currentTrack?.album?.images],
|
||||||
@ -94,10 +98,11 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||||
|
|
||||||
return PopScope(
|
// ignore: deprecated_member_use
|
||||||
canPop: false,
|
return WillPopScope(
|
||||||
onPopInvoked: (didPop) async {
|
onWillPop: () async {
|
||||||
panelController.close();
|
await panelController.close();
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
child: IconTheme(
|
child: IconTheme(
|
||||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||||
@ -137,11 +142,31 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
onPressed: panelController.close,
|
onPressed: panelController.close,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
if (currentTrack is YoutubeSourcedTrack)
|
||||||
|
TextButton.icon(
|
||||||
|
icon: Assets.logos.songlinkTransparent.image(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
color: bodyTextColor,
|
||||||
|
),
|
||||||
|
label: Text(context.l10n.song_link),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: bodyTextColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
final url =
|
||||||
|
"https://song.link/s/${currentTrack.id}";
|
||||||
|
|
||||||
|
launchUrlString(url);
|
||||||
|
},
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(SpotubeIcons.info, size: 18),
|
icon: const Icon(SpotubeIcons.info, size: 18),
|
||||||
tooltip: context.l10n.details,
|
tooltip: context.l10n.details,
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
foregroundColor: bodyTextColor),
|
foregroundColor: bodyTextColor,
|
||||||
|
),
|
||||||
onPressed: currentTrack == null
|
onPressed: currentTrack == null
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@ -217,19 +242,15 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (isLocalTrack)
|
if (isLocalTrack)
|
||||||
Text(
|
Text(
|
||||||
TypeConversionUtils.artists_X_String<
|
currentTrack.artists?.asString() ?? "",
|
||||||
Artist>(
|
|
||||||
currentTrack?.artists ?? [],
|
|
||||||
),
|
|
||||||
style: theme.textTheme.bodyMedium!.copyWith(
|
style: theme.textTheme.bodyMedium!.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: bodyTextColor,
|
color: bodyTextColor,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
TypeConversionUtils
|
ArtistLink(
|
||||||
.artists_X_ClickableArtists(
|
artists: currentTrack?.artists ?? [],
|
||||||
currentTrack?.artists ?? [],
|
|
||||||
textStyle:
|
textStyle:
|
||||||
theme.textTheme.bodyMedium!.copyWith(
|
theme.textTheme.bodyMedium!.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -285,10 +306,24 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
.height *
|
.height *
|
||||||
.7,
|
.7,
|
||||||
),
|
),
|
||||||
builder: (context) {
|
builder: (context) => Consumer(
|
||||||
return const PlayerQueue(
|
builder: (context, ref, _) {
|
||||||
floating: false);
|
final playlist = ref.watch(
|
||||||
},
|
proxyPlaylistProvider,
|
||||||
|
);
|
||||||
|
final playlistNotifier =
|
||||||
|
ref.read(
|
||||||
|
proxyPlaylistProvider
|
||||||
|
.notifier,
|
||||||
|
);
|
||||||
|
return PlayerQueue
|
||||||
|
.fromProxyPlaylistNotifier(
|
||||||
|
floating: false,
|
||||||
|
playlist: playlist,
|
||||||
|
notifier: playlistNotifier,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null),
|
: null),
|
||||||
@ -346,11 +381,21 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
enabledThumbRadius: 8,
|
enabledThumbRadius: 8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding:
|
||||||
child: VolumeSlider(
|
const EdgeInsets.symmetric(horizontal: 16),
|
||||||
fullWidth: true,
|
child: Consumer(builder: (context, ref, _) {
|
||||||
),
|
final volume = ref.watch(volumeProvider);
|
||||||
|
return VolumeSlider(
|
||||||
|
fullWidth: true,
|
||||||
|
value: volume,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(volumeProvider.notifier)
|
||||||
|
.setVolume(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -3,12 +3,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:spotify/spotify.dart' hide Offset;
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/player/sibling_tracks_sheet.dart';
|
import 'package:spotube/components/player/sibling_tracks_sheet.dart';
|
||||||
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
|
||||||
import 'package:spotube/components/shared/heart_button.dart';
|
import 'package:spotube/components/shared/heart_button.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
@ -17,7 +16,6 @@ import 'package:spotube/provider/download_manager_provider.dart';
|
|||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/sleep_timer_provider.dart';
|
import 'package:spotube/provider/sleep_timer_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlayerActions extends HookConsumerWidget {
|
class PlayerActions extends HookConsumerWidget {
|
||||||
final MainAxisAlignment mainAxisAlignment;
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
@ -29,14 +27,13 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
this.floatingQueue = true,
|
this.floatingQueue = true,
|
||||||
this.showQueue = true,
|
this.showQueue = true,
|
||||||
this.extraActions,
|
this.extraActions,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
final logger = getLogger(PlayerActions);
|
final logger = getLogger(PlayerActions);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
|
||||||
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
final isLocalTrack = playlist.activeTrack is LocalTrack;
|
||||||
ref.watch(downloadManagerProvider);
|
ref.watch(downloadManagerProvider);
|
||||||
final downloader = ref.watch(downloadManagerProvider.notifier);
|
final downloader = ref.watch(downloadManagerProvider.notifier);
|
||||||
@ -49,19 +46,17 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
final localTracks = [] /* ref.watch(localTracksProvider).value */;
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final sleepTimer = ref.watch(SleepTimerNotifier.provider);
|
final sleepTimer = ref.watch(sleepTimerProvider);
|
||||||
final sleepTimerNotifier = ref.watch(SleepTimerNotifier.notifier);
|
final sleepTimerNotifier = ref.watch(sleepTimerProvider.notifier);
|
||||||
|
|
||||||
final isDownloaded = useMemoized(() {
|
final isDownloaded = useMemoized(() {
|
||||||
return localTracks.any(
|
return localTracks.any(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.name == playlist.activeTrack?.name &&
|
element.name == playlist.activeTrack?.name &&
|
||||||
element.album?.name == playlist.activeTrack?.album?.name &&
|
element.album?.name == playlist.activeTrack?.album?.name &&
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
element.artists?.asString() ==
|
||||||
element.artists ?? []) ==
|
playlist.activeTrack?.artists?.asString(),
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
|
||||||
playlist.activeTrack?.artists ?? []),
|
|
||||||
) ==
|
) ==
|
||||||
true;
|
true;
|
||||||
}, [localTracks, playlist.activeTrack]);
|
}, [localTracks, playlist.activeTrack]);
|
||||||
|
|||||||
@ -21,8 +21,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
PlayerControls({
|
PlayerControls({
|
||||||
this.palette,
|
this.palette,
|
||||||
this.compact = false,
|
this.compact = false,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final logger = getLogger(PlayerControls);
|
final logger = getLogger(PlayerControls);
|
||||||
|
|
||||||
@ -43,8 +43,8 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
SeekIntent: SeekAction(),
|
SeekIntent: SeekAction(),
|
||||||
},
|
},
|
||||||
[]);
|
[]);
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
@ -256,20 +256,16 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
onPressed: playlist.isFetching == true
|
onPressed: playlist.isFetching == true
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
switch (await audioPlayer.loopMode) {
|
audioPlayer.setLoopMode(
|
||||||
case PlaybackLoopMode.all:
|
switch (loopMode) {
|
||||||
audioPlayer
|
PlaybackLoopMode.all =>
|
||||||
.setLoopMode(PlaybackLoopMode.one);
|
PlaybackLoopMode.one,
|
||||||
break;
|
PlaybackLoopMode.one =>
|
||||||
case PlaybackLoopMode.one:
|
PlaybackLoopMode.none,
|
||||||
audioPlayer
|
PlaybackLoopMode.none =>
|
||||||
.setLoopMode(PlaybackLoopMode.none);
|
PlaybackLoopMode.all,
|
||||||
break;
|
},
|
||||||
case PlaybackLoopMode.none:
|
);
|
||||||
audioPlayer
|
|
||||||
.setLoopMode(PlaybackLoopMode.all);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,16 +19,15 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
|
|
||||||
const PlayerOverlay({
|
const PlayerOverlay({
|
||||||
required this.albumArt,
|
required this.albumArt,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final canShow = ref.watch(
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
ProxyPlaylistNotifier.provider.select((s) => s.active != null),
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
);
|
final canShow = playlist.activeTrack != null;
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
|
|
||||||
@ -115,7 +114,7 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: PlayerTrackDetails(
|
child: PlayerTrackDetails(
|
||||||
albumArt: albumArt,
|
track: playlist.activeTrack,
|
||||||
color: textColor,
|
color: textColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -5,30 +5,55 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
import 'package:spotube/components/shared/fallbacks/not_found.dart';
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||||
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
import 'package:spotube/components/shared/track_tile/track_tile.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
import 'package:spotube/hooks/controllers/use_auto_scroll_controller.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlayerQueue extends HookConsumerWidget {
|
class PlayerQueue extends HookConsumerWidget {
|
||||||
final bool floating;
|
final bool floating;
|
||||||
|
final ProxyPlaylist playlist;
|
||||||
|
|
||||||
|
final Future<void> Function(Track track) onJump;
|
||||||
|
final Future<void> Function(String trackId) onRemove;
|
||||||
|
final Future<void> Function(int oldIndex, int newIndex) onReorder;
|
||||||
|
final Future<void> Function() onStop;
|
||||||
|
|
||||||
const PlayerQueue({
|
const PlayerQueue({
|
||||||
this.floating = true,
|
this.floating = true,
|
||||||
Key? key,
|
required this.playlist,
|
||||||
}) : super(key: key);
|
required this.onJump,
|
||||||
|
required this.onRemove,
|
||||||
|
required this.onReorder,
|
||||||
|
required this.onStop,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
PlayerQueue.fromProxyPlaylistNotifier({
|
||||||
|
this.floating = true,
|
||||||
|
required this.playlist,
|
||||||
|
required ProxyPlaylistNotifier notifier,
|
||||||
|
super.key,
|
||||||
|
}) : onJump = notifier.jumpToTrack,
|
||||||
|
onRemove = notifier.removeTrack,
|
||||||
|
onReorder = notifier.moveTrack,
|
||||||
|
onStop = notifier.stop;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
|
||||||
final controller = useAutoScrollController();
|
final controller = useAutoScrollController();
|
||||||
final searchText = useState('');
|
final searchText = useState('');
|
||||||
|
|
||||||
@ -44,7 +69,6 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
topRight: Radius.circular(10),
|
topRight: Radius.circular(10),
|
||||||
);
|
);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
|
||||||
final headlineColor = theme.textTheme.headlineSmall?.color;
|
final headlineColor = theme.textTheme.headlineSmall?.color;
|
||||||
|
|
||||||
final filteredTracks = useMemoized(
|
final filteredTracks = useMemoized(
|
||||||
@ -55,7 +79,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
return tracks
|
return tracks
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
weightedRatio(
|
weightedRatio(
|
||||||
'${e.name!} - ${TypeConversionUtils.artists_X_String(e.artists!)}',
|
'${e.name!} - ${e.artists?.asString() ?? ""}',
|
||||||
searchText.value,
|
searchText.value,
|
||||||
),
|
),
|
||||||
e
|
e
|
||||||
@ -83,201 +107,202 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
return const NotFound(vertical: true);
|
return const NotFound(vertical: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClipRRect(
|
return LayoutBuilder(
|
||||||
borderRadius: borderRadius,
|
builder: (context, constrains) {
|
||||||
clipBehavior: Clip.hardEdge,
|
return ClipRRect(
|
||||||
child: BackdropFilter(
|
borderRadius: borderRadius,
|
||||||
filter: ImageFilter.blur(
|
clipBehavior: Clip.hardEdge,
|
||||||
sigmaX: 15,
|
child: BackdropFilter(
|
||||||
sigmaY: 15,
|
filter: ImageFilter.blur(
|
||||||
),
|
sigmaX: 15,
|
||||||
child: Container(
|
sigmaY: 15,
|
||||||
padding: const EdgeInsets.only(
|
),
|
||||||
top: 5.0,
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.only(
|
||||||
decoration: BoxDecoration(
|
top: 5.0,
|
||||||
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
),
|
||||||
borderRadius: borderRadius,
|
decoration: BoxDecoration(
|
||||||
),
|
color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
|
||||||
child: CallbackShortcuts(
|
borderRadius: borderRadius,
|
||||||
bindings: {
|
),
|
||||||
LogicalKeySet(LogicalKeyboardKey.escape): () {
|
child: CallbackShortcuts(
|
||||||
if (!isSearching.value) {
|
bindings: {
|
||||||
Navigator.of(context).pop();
|
LogicalKeySet(LogicalKeyboardKey.escape): () {
|
||||||
}
|
if (!isSearching.value) {
|
||||||
isSearching.value = false;
|
Navigator.of(context).pop();
|
||||||
searchText.value = '';
|
}
|
||||||
}
|
isSearching.value = false;
|
||||||
},
|
searchText.value = '';
|
||||||
child: Column(
|
}
|
||||||
children: [
|
},
|
||||||
if (!floating)
|
child: InterScrollbar(
|
||||||
Container(
|
controller: controller,
|
||||||
height: 5,
|
child: CustomScrollView(
|
||||||
width: 100,
|
controller: controller,
|
||||||
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
slivers: [
|
||||||
decoration: BoxDecoration(
|
if (!floating)
|
||||||
color: headlineColor,
|
SliverToBoxAdapter(
|
||||||
borderRadius: BorderRadius.circular(20),
|
child: Center(
|
||||||
),
|
child: Container(
|
||||||
),
|
height: 5,
|
||||||
Row(
|
width: 100,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
margin: const EdgeInsets.only(bottom: 5, top: 2),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
decoration: BoxDecoration(
|
||||||
children: [
|
color: headlineColor,
|
||||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
borderRadius: BorderRadius.circular(20),
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
Text(
|
|
||||||
context.l10n.tracks_in_queue(tracks.length),
|
|
||||||
style: TextStyle(
|
|
||||||
color: headlineColor,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
if (mediaQuery.mdAndUp || isSearching.value)
|
|
||||||
TextField(
|
|
||||||
onChanged: (value) {
|
|
||||||
searchText.value = value;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: context.l10n.search,
|
|
||||||
isDense: true,
|
|
||||||
prefixIcon: mediaQuery.smAndDown
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_ios_new_outlined,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
isSearching.value = false;
|
|
||||||
searchText.value = '';
|
|
||||||
},
|
|
||||||
style: IconButton.styleFrom(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
minimumSize: const Size.square(20),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(SpotubeIcons.filter),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: 40,
|
|
||||||
maxWidth: mediaQuery.smAndDown
|
|
||||||
? mediaQuery.size.width - 40
|
|
||||||
: 300,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
IconButton.filledTonal(
|
|
||||||
icon: const Icon(SpotubeIcons.filter),
|
|
||||||
onPressed: () {
|
|
||||||
isSearching.value = !isSearching.value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
FilledButton(
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
theme.scaffoldBackgroundColor.withOpacity(0.5),
|
|
||||||
foregroundColor: theme.textTheme.headlineSmall?.color,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(SpotubeIcons.playlistRemove),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
Text(context.l10n.clear_all),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
playlistNotifier.stop();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
if (!isSearching.value && searchText.value.isEmpty)
|
|
||||||
Flexible(
|
|
||||||
child: ReorderableListView.builder(
|
|
||||||
onReorder: (oldIndex, newIndex) {
|
|
||||||
playlistNotifier.moveTrack(oldIndex, newIndex);
|
|
||||||
},
|
|
||||||
scrollController: controller,
|
|
||||||
itemCount: tracks.length,
|
|
||||||
shrinkWrap: true,
|
|
||||||
buildDefaultDragHandles: false,
|
|
||||||
onReorderStart: (index) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
},
|
|
||||||
onReorderEnd: (index) {
|
|
||||||
HapticFeedback.selectionClick();
|
|
||||||
},
|
|
||||||
itemBuilder: (context, i) {
|
|
||||||
final track = tracks.elementAt(i);
|
|
||||||
return AutoScrollTag(
|
|
||||||
key: ValueKey(i),
|
|
||||||
controller: controller,
|
|
||||||
index: i,
|
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: TrackTile(
|
|
||||||
index: i,
|
|
||||||
track: track,
|
|
||||||
onTap: () async {
|
|
||||||
if (playlist.activeTrack?.id == track.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await playlistNotifier.jumpToTrack(track);
|
|
||||||
},
|
|
||||||
leadingActions: [
|
|
||||||
ReorderableDragStartListener(
|
|
||||||
index: i,
|
|
||||||
child: const Icon(SpotubeIcons.dragHandle),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
SliverAppBar(
|
||||||
),
|
floating: true,
|
||||||
)
|
pinned: false,
|
||||||
else
|
snap: false,
|
||||||
Flexible(
|
backgroundColor: Colors.transparent,
|
||||||
child: InterScrollbar(
|
elevation: 0,
|
||||||
controller: controller,
|
automaticallyImplyLeading: false,
|
||||||
child: ListView.builder(
|
title: BackdropFilter(
|
||||||
controller: controller,
|
filter: ImageFilter.blur(
|
||||||
|
sigmaX: 10,
|
||||||
|
sigmaY: 10,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: mediaQuery.mdAndUp || !isSearching.value
|
||||||
|
? Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
context.l10n
|
||||||
|
.tracks_in_queue(tracks.length),
|
||||||
|
style: TextStyle(
|
||||||
|
color: headlineColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (mediaQuery.mdAndUp || isSearching.value)
|
||||||
|
TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
searchText.value = value;
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: context.l10n.search,
|
||||||
|
isDense: true,
|
||||||
|
prefixIcon: mediaQuery.smAndDown
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_new_outlined,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
isSearching.value = false;
|
||||||
|
searchText.value = '';
|
||||||
|
},
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: const Size.square(20),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(SpotubeIcons.filter),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 40,
|
||||||
|
maxWidth: mediaQuery.smAndDown
|
||||||
|
? mediaQuery.size.width - 40
|
||||||
|
: 300,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton.filledTonal(
|
||||||
|
icon: const Icon(SpotubeIcons.filter),
|
||||||
|
onPressed: () {
|
||||||
|
isSearching.value = !isSearching.value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (mediaQuery.mdAndUp || !isSearching.value) ...[
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
FilledButton(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: theme.scaffoldBackgroundColor
|
||||||
|
.withOpacity(0.5),
|
||||||
|
foregroundColor:
|
||||||
|
theme.textTheme.headlineSmall?.color,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(SpotubeIcons.playlistRemove),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(context.l10n.clear_all),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
onStop();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SliverGap(10),
|
||||||
|
SliverReorderableList(
|
||||||
|
onReorder: onReorder,
|
||||||
itemCount: filteredTracks.length,
|
itemCount: filteredTracks.length,
|
||||||
|
onReorderStart: (index) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
|
onReorderEnd: (index) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
},
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
final track = filteredTracks.elementAt(i);
|
final track = filteredTracks.elementAt(i);
|
||||||
return Padding(
|
return AutoScrollTag(
|
||||||
padding:
|
key: ValueKey<int>(i),
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
controller: controller,
|
||||||
child: TrackTile(
|
index: i,
|
||||||
index: i,
|
child: Material(
|
||||||
track: track,
|
color: Colors.transparent,
|
||||||
onTap: () async {
|
child: TrackTile(
|
||||||
if (playlist.activeTrack?.id == track.id) {
|
playlist: playlist,
|
||||||
return;
|
index: i,
|
||||||
}
|
track: track,
|
||||||
await playlistNotifier.jumpToTrack(track);
|
onTap: () async {
|
||||||
},
|
if (playlist.activeTrack?.id == track.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onJump(track);
|
||||||
|
},
|
||||||
|
leadingActions: [
|
||||||
|
if (!isSearching.value &&
|
||||||
|
searchText.value.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: ReorderableDragStartListener(
|
||||||
|
index: i,
|
||||||
|
child: const Icon(
|
||||||
|
SpotubeIcons.dragHandle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
const SliverGap(100),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,23 +4,24 @@ import 'package:spotify/spotify.dart';
|
|||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
|
import 'package:spotube/components/shared/links/artist_link.dart';
|
||||||
import 'package:spotube/components/shared/links/link_text.dart';
|
import 'package:spotube/components/shared/links/link_text.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlayerTrackDetails extends HookConsumerWidget {
|
class PlayerTrackDetails extends HookConsumerWidget {
|
||||||
final String? albumArt;
|
|
||||||
final Color? color;
|
final Color? color;
|
||||||
const PlayerTrackDetails({Key? key, this.albumArt, this.color})
|
final Track? track;
|
||||||
: super(key: key);
|
const PlayerTrackDetails({super.key, this.color, this.track});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final playback = ref.watch(ProxyPlaylistNotifier.provider);
|
final playback = ref.watch(proxyPlaylistProvider);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@ -34,7 +35,8 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: UniversalImage(
|
child: UniversalImage(
|
||||||
path: albumArt ?? "",
|
path: (track?.album?.images)
|
||||||
|
.asUrlString(placeholder: ImagePlaceholder.albumArt),
|
||||||
placeholder: Assets.albumPlaceholder.path,
|
placeholder: Assets.albumPlaceholder.path,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -55,9 +57,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
TypeConversionUtils.artists_X_String<Artist>(
|
playback.activeTrack?.artists?.asString() ?? "",
|
||||||
playback.activeTrack?.artists ?? [],
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: theme.textTheme.bodySmall!.copyWith(color: color),
|
style: theme.textTheme.bodySmall!.copyWith(color: color),
|
||||||
)
|
)
|
||||||
@ -76,8 +76,8 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
style: TextStyle(fontWeight: FontWeight.bold, color: color),
|
||||||
),
|
),
|
||||||
TypeConversionUtils.artists_X_ClickableArtists(
|
ArtistLink(
|
||||||
playback.activeTrack?.artists ?? [],
|
artists: playback.activeTrack?.artists ?? [],
|
||||||
onRouteChange: (route) {
|
onRouteChange: (route) {
|
||||||
ServiceUtils.push(context, route);
|
ServiceUtils.push(context, route);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -4,17 +4,18 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Offset;
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
|
||||||
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/hooks/utils/use_debounce.dart';
|
import 'package:spotube/hooks/utils/use_debounce.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
@ -24,7 +25,6 @@ import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
|||||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
final sourceInfoToIconMap = {
|
final sourceInfoToIconMap = {
|
||||||
YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
|
YoutubeSourceInfo: const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
|
||||||
@ -45,29 +45,30 @@ final sourceInfoToIconMap = {
|
|||||||
class SiblingTracksSheet extends HookConsumerWidget {
|
class SiblingTracksSheet extends HookConsumerWidget {
|
||||||
final bool floating;
|
final bool floating;
|
||||||
const SiblingTracksSheet({
|
const SiblingTracksSheet({
|
||||||
Key? key,
|
super.key,
|
||||||
this.floating = true,
|
this.floating = true,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
final searchMode = useState(preferences.searchMode);
|
final searchMode = useState(preferences.searchMode);
|
||||||
|
final activeTrackNotifier = ref.watch(activeSourcedTrackProvider.notifier);
|
||||||
|
final activeTrack =
|
||||||
|
ref.watch(activeSourcedTrackProvider) ?? playlist.activeTrack;
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
playlist.activeTrack?.name ?? "",
|
activeTrack?.name ?? "",
|
||||||
artists:
|
artists: activeTrack?.artists?.map((e) => e.name!).toList() ?? [],
|
||||||
playlist.activeTrack?.artists?.map((e) => e.name!).toList() ?? [],
|
|
||||||
onlyCleanArtist: true,
|
onlyCleanArtist: true,
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
final defaultSearchTerm =
|
final defaultSearchTerm =
|
||||||
"$title - ${TypeConversionUtils.artists_X_String<Artist>(playlist.activeTrack?.artists ?? [])}";
|
"$title - ${activeTrack?.artists?.asString() ?? ""}";
|
||||||
final searchController = useTextEditingController(
|
final searchController = useTextEditingController(
|
||||||
text: defaultSearchTerm,
|
text: defaultSearchTerm,
|
||||||
);
|
);
|
||||||
@ -91,8 +92,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
return siblingType.info;
|
return siblingType.info;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
final activeSourceInfo =
|
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo;
|
||||||
(playlist.activeTrack! as SourcedTrack).sourceInfo;
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
@ -112,8 +112,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
return siblingType.info;
|
return siblingType.info;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
final activeSourceInfo =
|
final activeSourceInfo = (activeTrack! as SourcedTrack).sourceInfo;
|
||||||
(playlist.activeTrack! as SourcedTrack).sourceInfo;
|
|
||||||
return searchResults
|
return searchResults
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
..insert(
|
..insert(
|
||||||
@ -124,18 +123,18 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
}, [
|
}, [
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchMode.value,
|
searchMode.value,
|
||||||
playlist.activeTrack,
|
activeTrack,
|
||||||
preferences.audioSource,
|
preferences.audioSource,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final siblings = useMemoized(
|
final siblings = useMemoized(
|
||||||
() => playlist.isFetching == false
|
() => playlist.isFetching == false
|
||||||
? [
|
? [
|
||||||
(playlist.activeTrack as SourcedTrack).sourceInfo,
|
(activeTrack as SourcedTrack).sourceInfo,
|
||||||
...(playlist.activeTrack as SourcedTrack).siblings,
|
...activeTrack.siblings,
|
||||||
]
|
]
|
||||||
: <SourceInfo>[],
|
: <SourceInfo>[],
|
||||||
[playlist.isFetching, playlist.activeTrack],
|
[playlist.isFetching, activeTrack],
|
||||||
);
|
);
|
||||||
|
|
||||||
final borderRadius = floating
|
final borderRadius = floating
|
||||||
@ -146,12 +145,11 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (playlist.activeTrack is SourcedTrack &&
|
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
|
||||||
(playlist.activeTrack as SourcedTrack).siblings.isEmpty) {
|
activeTrackNotifier.populateSibling();
|
||||||
playlistNotifier.populateSibling();
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [playlist.activeTrack]);
|
}, [activeTrack]);
|
||||||
|
|
||||||
final itemBuilder = useCallback(
|
final itemBuilder = useCallback(
|
||||||
(SourceInfo sourceInfo) {
|
(SourceInfo sourceInfo) {
|
||||||
@ -178,20 +176,18 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
enabled: playlist.isFetching != true,
|
enabled: playlist.isFetching != true,
|
||||||
selected: playlist.isFetching != true &&
|
selected: playlist.isFetching != true &&
|
||||||
sourceInfo.id ==
|
sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id,
|
||||||
(playlist.activeTrack as SourcedTrack).sourceInfo.id,
|
|
||||||
selectedTileColor: theme.popupMenuTheme.color,
|
selectedTileColor: theme.popupMenuTheme.color,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (playlist.isFetching == false &&
|
if (playlist.isFetching == false &&
|
||||||
sourceInfo.id !=
|
sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) {
|
||||||
(playlist.activeTrack as SourcedTrack).sourceInfo.id) {
|
activeTrackNotifier.swapSibling(sourceInfo);
|
||||||
playlistNotifier.swapSibling(sourceInfo);
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[playlist.isFetching, playlist.activeTrack, siblings],
|
[playlist.isFetching, activeTrack, siblings],
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
|||||||
@ -3,37 +3,39 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
|
||||||
|
|
||||||
class VolumeSlider extends HookConsumerWidget {
|
class VolumeSlider extends HookConsumerWidget {
|
||||||
final bool fullWidth;
|
final bool fullWidth;
|
||||||
|
|
||||||
|
final double value;
|
||||||
|
final ValueChanged<double> onChanged;
|
||||||
|
|
||||||
const VolumeSlider({
|
const VolumeSlider({
|
||||||
Key? key,
|
super.key,
|
||||||
this.fullWidth = false,
|
this.fullWidth = false,
|
||||||
}) : super(key: key);
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final volume = ref.watch(volumeProvider);
|
|
||||||
final volumeNotifier = ref.watch(volumeProvider.notifier);
|
|
||||||
|
|
||||||
var slider = Listener(
|
var slider = Listener(
|
||||||
onPointerSignal: (event) async {
|
onPointerSignal: (event) async {
|
||||||
if (event is PointerScrollEvent) {
|
if (event is PointerScrollEvent) {
|
||||||
if (event.scrollDelta.dy > 0) {
|
if (event.scrollDelta.dy > 0) {
|
||||||
final value = volume - .2;
|
final newValue = value - .2;
|
||||||
volumeNotifier.setVolume(value < 0 ? 0 : value);
|
onChanged(newValue < 0 ? 0 : newValue);
|
||||||
} else {
|
} else {
|
||||||
final value = volume + .2;
|
final newValue = value + .2;
|
||||||
volumeNotifier.setVolume(value > 1 ? 1 : value);
|
onChanged(newValue > 1 ? 1 : newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Slider(
|
child: Slider(
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
value: volume,
|
value: value,
|
||||||
onChanged: volumeNotifier.setVolume,
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return Row(
|
return Row(
|
||||||
@ -42,20 +44,20 @@ class VolumeSlider extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
volume == 0
|
value == 0
|
||||||
? SpotubeIcons.volumeMute
|
? SpotubeIcons.volumeMute
|
||||||
: volume <= 0.2
|
: value <= 0.2
|
||||||
? SpotubeIcons.volumeLow
|
? SpotubeIcons.volumeLow
|
||||||
: volume <= 0.6
|
: value <= 0.6
|
||||||
? SpotubeIcons.volumeMedium
|
? SpotubeIcons.volumeMedium
|
||||||
: SpotubeIcons.volumeHigh,
|
: SpotubeIcons.volumeHigh,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (volume == 0) {
|
if (value == 0) {
|
||||||
volumeNotifier.setVolume(1);
|
onChanged(1);
|
||||||
} else {
|
} else {
|
||||||
volumeNotifier.setVolume(0);
|
onChanged(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,77 +1,59 @@
|
|||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/shared/dialogs/select_device_dialog.dart';
|
||||||
import 'package:spotube/components/shared/playbutton_card.dart';
|
import 'package:spotube/components/shared/playbutton_card.dart';
|
||||||
import 'package:spotube/extensions/infinite_query.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/provider/connect/connect.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlaylistCard extends HookConsumerWidget {
|
class PlaylistCard extends HookConsumerWidget {
|
||||||
final PlaylistSimple playlist;
|
final PlaylistSimple playlist;
|
||||||
const PlaylistCard(
|
const PlaylistCard(
|
||||||
this.playlist, {
|
this.playlist, {
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlistQueue = ref.watch(proxyPlaylistProvider);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playlistNotifier = ref.watch(proxyPlaylistProvider.notifier);
|
||||||
final playing =
|
final playing =
|
||||||
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
|
||||||
final queryClient = QueryClient.of(context);
|
|
||||||
final tracks = useState<List<TrackSimple>?>(null);
|
|
||||||
bool isPlaylistPlaying = useMemoized(
|
bool isPlaylistPlaying = useMemoized(
|
||||||
() => playlistQueue.containsCollection(playlist.id!),
|
() => playlistQueue.containsCollection(playlist.id!),
|
||||||
[playlistQueue, playlist.id],
|
[playlistQueue, playlist.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
final updating = useState(false);
|
final updating = useState(false);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final me = ref.watch(meProvider);
|
||||||
final me = useQueries.user.me(ref);
|
|
||||||
|
|
||||||
Future<List<Track>> fetchAllTracks() async {
|
Future<List<Track>> fetchAllTracks() async {
|
||||||
if (playlist.id == 'user-liked-tracks') {
|
if (playlist.id == 'user-liked-tracks') {
|
||||||
return await queryClient.fetchQuery(
|
return await ref.read(likedTracksProvider.future);
|
||||||
"user-liked-tracks",
|
|
||||||
() => useQueries.playlist.likedTracks(spotify),
|
|
||||||
) ??
|
|
||||||
[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = queryClient.createInfiniteQuery<List<Track>, dynamic, int>(
|
await ref.read(playlistTracksProvider(playlist.id!).future);
|
||||||
"playlist-tracks/${playlist.id}",
|
|
||||||
(page) => useQueries.playlist.tracksOf(page, spotify, playlist.id!),
|
|
||||||
initialPage: 0,
|
|
||||||
nextPage: useQueries.playlist.tracksOfQueryNextPage,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await query.fetchAllTracks(
|
return ref.read(playlistTracksProvider(playlist.id!).notifier).fetchAll();
|
||||||
getAllTracks: () async {
|
|
||||||
final res =
|
|
||||||
await spotify.playlists.getTracksByPlaylistId(playlist.id!).all();
|
|
||||||
return res.toList();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
title: playlist.name!,
|
title: playlist.name!,
|
||||||
description: playlist.description,
|
description: playlist.description,
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(
|
imageUrl: playlist.images.asUrlString(
|
||||||
playlist.images,
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
isPlaying: isPlaylistPlaying,
|
isPlaying: isPlaylistPlaying,
|
||||||
isLoading:
|
isLoading:
|
||||||
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
|
(isPlaylistPlaying && playlistQueue.isFetching) || updating.value,
|
||||||
isOwner: playlist.owner?.id == me.data?.id && me.data?.id != null,
|
isOwner: playlist.owner?.id == me.asData?.value.id &&
|
||||||
|
me.asData?.value.id != null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ServiceUtils.push(
|
ServiceUtils.push(
|
||||||
context,
|
context,
|
||||||
@ -90,11 +72,21 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
List<Track> fetchedTracks = await fetchAllTracks();
|
List<Track> fetchedTracks = await fetchAllTracks();
|
||||||
|
|
||||||
if (fetchedTracks.isEmpty) return;
|
if (fetchedTracks.isEmpty || !context.mounted) return;
|
||||||
|
|
||||||
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
if (isRemoteDevice) {
|
||||||
tracks.value = fetchedTracks;
|
final remotePlayback = ref.read(connectProvider.notifier);
|
||||||
|
await remotePlayback.load(
|
||||||
|
WebSocketLoadEventData(
|
||||||
|
tracks: fetchedTracks,
|
||||||
|
collectionId: playlist.id!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await playlistNotifier.load(fetchedTracks, autoPlay: true);
|
||||||
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
@ -112,10 +104,9 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
playlistNotifier.addTracks(fetchedTracks);
|
playlistNotifier.addTracks(fetchedTracks);
|
||||||
playlistNotifier.addCollection(playlist.id!);
|
playlistNotifier.addCollection(playlist.id!);
|
||||||
tracks.value = fetchedTracks;
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
final snackbar = SnackBar(
|
final snackbar = SnackBar(
|
||||||
content: Text("Added ${tracks.value?.length} tracks to queue"),
|
content: Text("Added ${fetchedTracks.length} tracks to queue"),
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: "Undo",
|
label: "Undo",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:form_validator/form_validator.dart';
|
import 'package:form_validator/form_validator.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -13,21 +14,19 @@ import 'package:spotube/collections/spotube_icons.dart';
|
|||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/provider/spotify_provider.dart';
|
||||||
import 'package:spotube/services/mutations/mutations.dart';
|
|
||||||
import 'package:spotube/services/mutations/playlist.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlaylistCreateDialog extends HookConsumerWidget {
|
class PlaylistCreateDialog extends HookConsumerWidget {
|
||||||
/// Track ids to add to the playlist
|
/// Track ids to add to the playlist
|
||||||
final List<String> trackIds;
|
final List<String> trackIds;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
PlaylistCreateDialog({
|
PlaylistCreateDialog({
|
||||||
Key? key,
|
super.key,
|
||||||
this.trackIds = const [],
|
this.trackIds = const [],
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@ -37,13 +36,16 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: HookBuilder(builder: (context) {
|
body: HookBuilder(builder: (context) {
|
||||||
final userPlaylists = useQueries.playlist.ofMine(ref);
|
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||||
|
final playlist = ref.watch(playlistProvider(playlistId ?? ""));
|
||||||
|
final playlistNotifier =
|
||||||
|
ref.watch(playlistProvider(playlistId ?? "").notifier);
|
||||||
|
|
||||||
final updatingPlaylist = useMemoized(
|
final updatingPlaylist = useMemoized(
|
||||||
() => userPlaylists.pages
|
() => userPlaylists.asData?.value.items
|
||||||
.expand((p) => p.items ?? <PlaylistSimple>[])
|
|
||||||
.firstWhereOrNull((playlist) => playlist.id == playlistId),
|
.firstWhereOrNull((playlist) => playlist.id == playlistId),
|
||||||
[
|
[
|
||||||
userPlaylists.pages,
|
userPlaylists.asData?.value.items,
|
||||||
playlistId,
|
playlistId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -84,28 +86,10 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}, [scaffold, l10n, theme]);
|
}, [scaffold, l10n, theme]);
|
||||||
|
|
||||||
final playlistCreateMutation = useMutations.playlist.create(
|
|
||||||
ref,
|
|
||||||
trackIds: trackIds,
|
|
||||||
onData: (value) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
onError: onError,
|
|
||||||
);
|
|
||||||
|
|
||||||
final playlistUpdateMutation = useMutations.playlist.update(
|
|
||||||
ref,
|
|
||||||
playlistId: playlistId,
|
|
||||||
onData: (value) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
onError: onError,
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> onCreate() async {
|
Future<void> onCreate() async {
|
||||||
if (!formKey.currentState!.validate()) return;
|
if (!formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
final PlaylistCRUDVariables payload = (
|
final PlaylistInput payload = (
|
||||||
playlistName: playlistName.text,
|
playlistName: playlistName.text,
|
||||||
collaborative: collaborative.value,
|
collaborative: collaborative.value,
|
||||||
public: public.value,
|
public: public.value,
|
||||||
@ -118,9 +102,14 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isUpdatingPlaylist) {
|
if (isUpdatingPlaylist) {
|
||||||
await playlistUpdateMutation.mutate(payload);
|
await playlistNotifier.modify(payload, onError);
|
||||||
} else {
|
} else {
|
||||||
await playlistCreateMutation.mutate(payload);
|
await playlistNotifier.create(payload, onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mounted &&
|
||||||
|
!ref.read(playlistProvider(playlistId ?? "")).hasError) {
|
||||||
|
context.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +127,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: onCreate,
|
onPressed: playlist.isLoading ? null : onCreate,
|
||||||
child: Text(
|
child: Text(
|
||||||
isUpdatingPlaylist
|
isUpdatingPlaylist
|
||||||
? context.l10n.update
|
? context.l10n.update
|
||||||
@ -174,8 +163,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
UniversalImage(
|
UniversalImage(
|
||||||
path: field.value?.path ??
|
path: field.value?.path ??
|
||||||
TypeConversionUtils.image_X_UrlString(
|
(updatingPlaylist?.images).asUrlString(
|
||||||
updatingPlaylist?.images,
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
height: 200,
|
height: 200,
|
||||||
@ -275,7 +263,7 @@ class PlaylistCreateDialog extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PlaylistCreateDialogButton extends HookConsumerWidget {
|
class PlaylistCreateDialogButton extends HookConsumerWidget {
|
||||||
const PlaylistCreateDialogButton({Key? key}) : super(key: key);
|
const PlaylistCreateDialogButton({super.key});
|
||||||
|
|
||||||
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
showPlaylistDialog(BuildContext context, SpotifyApi spotify) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:spotube/components/player/player_controls.dart';
|
|||||||
import 'package:spotube/components/player/volume_slider.dart';
|
import 'package:spotube/components/player/volume_slider.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -21,17 +22,17 @@ import 'package:spotube/provider/authentication_provider.dart';
|
|||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class BottomPlayer extends HookConsumerWidget {
|
class BottomPlayer extends HookConsumerWidget {
|
||||||
BottomPlayer({Key? key}) : super(key: key);
|
BottomPlayer({super.key});
|
||||||
|
|
||||||
final logger = getLogger(BottomPlayer);
|
final logger = getLogger(BottomPlayer);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
final playlist = ref.watch(proxyPlaylistProvider);
|
||||||
final layoutMode =
|
final layoutMode =
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
@ -39,8 +40,7 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
|
|
||||||
String albumArt = useMemoized(
|
String albumArt = useMemoized(
|
||||||
() => playlist.activeTrack?.album?.images?.isNotEmpty == true
|
() => playlist.activeTrack?.album?.images?.isNotEmpty == true
|
||||||
? TypeConversionUtils.image_X_UrlString(
|
? (playlist.activeTrack?.album?.images).asUrlString(
|
||||||
playlist.activeTrack?.album?.images,
|
|
||||||
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
index: (playlist.activeTrack?.album?.images?.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
)
|
)
|
||||||
@ -74,7 +74,9 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: PlayerTrackDetails(albumArt: albumArt)),
|
Expanded(
|
||||||
|
child: PlayerTrackDetails(track: playlist.activeTrack),
|
||||||
|
),
|
||||||
// controls
|
// controls
|
||||||
Flexible(
|
Flexible(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
@ -122,10 +124,20 @@ class BottomPlayer extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
height: 40,
|
height: 40,
|
||||||
constraints: const BoxConstraints(maxWidth: 250),
|
constraints: const BoxConstraints(maxWidth: 250),
|
||||||
child: const VolumeSlider(),
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: Consumer(builder: (context, ref, _) {
|
||||||
|
final volume = ref.watch(volumeProvider);
|
||||||
|
return VolumeSlider(
|
||||||
|
fullWidth: true,
|
||||||
|
value: volume,
|
||||||
|
onChanged: (value) {
|
||||||
|
ref.read(volumeProvider.notifier).setVolume(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -8,19 +9,21 @@ import 'package:sidebarx/sidebarx.dart';
|
|||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
import 'package:spotube/collections/side_bar_tiles.dart';
|
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/connect/connect_device.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||||
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/authentication_provider.dart';
|
import 'package:spotube/provider/authentication_provider.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int? selectedIndex;
|
final int? selectedIndex;
|
||||||
@ -31,8 +34,8 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
required this.onSelectedIndexChanged,
|
required this.onSelectedIndexChanged,
|
||||||
required this.child,
|
required this.child,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
static Widget brandLogo() {
|
static Widget brandLogo() {
|
||||||
return Container(
|
return Container(
|
||||||
@ -195,7 +198,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SidebarHeader extends HookWidget {
|
class SidebarHeader extends HookWidget {
|
||||||
const SidebarHeader({Key? key}) : super(key: key);
|
const SidebarHeader({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -234,23 +237,22 @@ class SidebarHeader extends HookWidget {
|
|||||||
|
|
||||||
class SidebarFooter extends HookConsumerWidget {
|
class SidebarFooter extends HookConsumerWidget {
|
||||||
const SidebarFooter({
|
const SidebarFooter({
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final me = useQueries.user.me(ref);
|
final me = ref.watch(meProvider);
|
||||||
final data = me.data;
|
final data = me.asData?.value;
|
||||||
|
|
||||||
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
final avatarImg = (data?.images).asUrlString(
|
||||||
data?.images,
|
|
||||||
index: (data?.images?.length ?? 1) - 1,
|
index: (data?.images?.length ?? 1) - 1,
|
||||||
placeholder: ImagePlaceholder.artist,
|
placeholder: ImagePlaceholder.artist,
|
||||||
);
|
);
|
||||||
|
|
||||||
final auth = ref.watch(AuthenticationNotifier.provider);
|
final auth = ref.watch(authenticationProvider);
|
||||||
|
|
||||||
if (mediaQuery.mdAndDown) {
|
if (mediaQuery.mdAndDown) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
@ -262,43 +264,56 @@ class SidebarFooter extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.only(left: 12),
|
padding: const EdgeInsets.only(left: 12),
|
||||||
width: 250,
|
width: 250,
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
if (auth != null && data == null)
|
const ConnectDeviceButton.sidebar(),
|
||||||
const CircularProgressIndicator()
|
const Gap(10),
|
||||||
else if (data != null)
|
Row(
|
||||||
Flexible(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
if (auth != null && data == null)
|
||||||
backgroundImage: UniversalImage.imageProvider(avatarImg),
|
const CircularProgressIndicator()
|
||||||
onBackgroundImageError: (exception, stackTrace) =>
|
else if (data != null)
|
||||||
Assets.userPlaceholder.image(
|
Flexible(
|
||||||
height: 16,
|
child: InkWell(
|
||||||
width: 16,
|
onTap: () {
|
||||||
|
ServiceUtils.push(context, "/profile");
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundImage:
|
||||||
|
UniversalImage.imageProvider(avatarImg),
|
||||||
|
onBackgroundImageError: (exception, stackTrace) =>
|
||||||
|
Assets.userPlaceholder.image(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
data.displayName ?? context.l10n.guest,
|
||||||
|
maxLines: 1,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
style: theme.textTheme.bodyMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
),
|
||||||
Flexible(
|
IconButton(
|
||||||
child: Text(
|
icon: const Icon(SpotubeIcons.settings),
|
||||||
data.displayName ?? context.l10n.guest,
|
onPressed: () {
|
||||||
maxLines: 1,
|
Sidebar.goToSettings(context);
|
||||||
softWrap: false,
|
},
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: theme.textTheme.bodyMedium
|
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
IconButton(
|
|
||||||
icon: const Icon(SpotubeIcons.settings),
|
|
||||||
onPressed: () {
|
|
||||||
Sidebar.goToSettings(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -23,8 +23,8 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
const SpotubeNavigationBar({
|
const SpotubeNavigationBar({
|
||||||
required this.selectedIndex,
|
required this.selectedIndex,
|
||||||
required this.onSelectedIndexChanged,
|
required this.onSelectedIndexChanged,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import 'package:system_theme/system_theme.dart';
|
|||||||
class SpotubeColor extends Color {
|
class SpotubeColor extends Color {
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
const SpotubeColor(int color, {required this.name}) : super(color);
|
const SpotubeColor(super.color, {required this.name});
|
||||||
|
|
||||||
const SpotubeColor.from(int value, {required this.name}) : super(value);
|
const SpotubeColor.from(super.value, {required this.name});
|
||||||
|
|
||||||
factory SpotubeColor.fromString(String string) {
|
factory SpotubeColor.fromString(String string) {
|
||||||
final slices = string.split(":");
|
final slices = string.split(":");
|
||||||
@ -44,7 +44,7 @@ final Set<SpotubeColor> colorsMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ColorSchemePickerDialog extends HookConsumerWidget {
|
class ColorSchemePickerDialog extends HookConsumerWidget {
|
||||||
const ColorSchemePickerDialog({Key? key}) : super(key: key);
|
const ColorSchemePickerDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
@ -119,8 +119,8 @@ class ColorTile extends StatelessWidget {
|
|||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.tooltip = "",
|
this.tooltip = "",
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
factory ColorTile.compact({
|
factory ColorTile.compact({
|
||||||
required Color color,
|
required Color color,
|
||||||
|
|||||||
@ -12,13 +12,13 @@ class Action extends StatelessWidget {
|
|||||||
final bool isExpanded;
|
final bool isExpanded;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
const Action({
|
const Action({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.text,
|
required this.text,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
this.isExpanded = true,
|
this.isExpanded = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
|
|
||||||
class AdaptiveSelectTile<T> extends HookWidget {
|
class AdaptiveSelectTile<T> extends HookWidget {
|
||||||
@ -38,11 +39,22 @@ class AdaptiveSelectTile<T> extends HookWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final rawControl = DropdownButton<T>(
|
final rawControl = DecoratedBox(
|
||||||
items: options,
|
decoration: BoxDecoration(
|
||||||
value: value,
|
color: theme.colorScheme.secondaryContainer,
|
||||||
onChanged: onChanged,
|
borderRadius: BorderRadius.circular(10),
|
||||||
menuMaxHeight: mediaQuery.size.height * 0.6,
|
),
|
||||||
|
child: DropdownButton<T>(
|
||||||
|
items: options,
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
menuMaxHeight: mediaQuery.size.height * 0.6,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
icon: const Icon(SpotubeIcons.angleDown),
|
||||||
|
dropdownColor: theme.colorScheme.secondaryContainer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final controlPlaceholder = useMemoized(
|
final controlPlaceholder = useMemoized(
|
||||||
() => options
|
() => options
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
|
|
||||||
class AnimateGradient extends HookWidget {
|
class AnimateGradient extends HookWidget {
|
||||||
const AnimateGradient({
|
const AnimateGradient({
|
||||||
Key? key,
|
super.key,
|
||||||
required this.primaryColors,
|
required this.primaryColors,
|
||||||
required this.secondaryColors,
|
required this.secondaryColors,
|
||||||
this.child,
|
this.child,
|
||||||
@ -17,8 +17,7 @@ class AnimateGradient extends HookWidget {
|
|||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
}) : assert(primaryColors.length >= 2),
|
}) : assert(primaryColors.length >= 2),
|
||||||
assert(primaryColors.length == secondaryColors.length),
|
assert(primaryColors.length == secondaryColors.length),
|
||||||
_controller = controller,
|
_controller = controller;
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
/// [controller]: pass this to have a fine control over the [Animation]
|
/// [controller]: pass this to have a fine control over the [Animation]
|
||||||
final AnimationController? _controller;
|
final AnimationController? _controller;
|
||||||
|
|||||||
@ -79,7 +79,7 @@ class BorderedText extends StatelessWidget {
|
|||||||
strutStyle: child.strutStyle,
|
strutStyle: child.strutStyle,
|
||||||
textAlign: child.textAlign,
|
textAlign: child.textAlign,
|
||||||
textDirection: child.textDirection,
|
textDirection: child.textDirection,
|
||||||
textScaleFactor: child.textScaleFactor,
|
textScaler: child.textScaler,
|
||||||
),
|
),
|
||||||
child,
|
child,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -11,12 +11,12 @@ class CompactSearch extends HookWidget {
|
|||||||
final Color? iconColor;
|
final Color? iconColor;
|
||||||
|
|
||||||
const CompactSearch({
|
const CompactSearch({
|
||||||
Key? key,
|
super.key,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.placeholder = "Search...",
|
this.placeholder = "Search...",
|
||||||
this.icon = SpotubeIcons.search,
|
this.icon = SpotubeIcons.search,
|
||||||
this.iconColor,
|
this.iconColor,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import 'package:spotube/extensions/constrains.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
|
||||||
class ConfirmDownloadDialog extends StatelessWidget {
|
class ConfirmDownloadDialog extends StatelessWidget {
|
||||||
const ConfirmDownloadDialog({Key? key}) : super(key: key);
|
const ConfirmDownloadDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -82,7 +82,7 @@ class ConfirmDownloadDialog extends StatelessWidget {
|
|||||||
|
|
||||||
class BulletPoint extends StatelessWidget {
|
class BulletPoint extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
const BulletPoint(this.text, {Key? key}) : super(key: key);
|
const BulletPoint(this.text, {super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import 'package:spotube/extensions/context.dart';
|
|||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
class PipedDownDialog extends HookConsumerWidget {
|
class PipedDownDialog extends HookConsumerWidget {
|
||||||
const PipedDownDialog({Key? key}) : super(key: key);
|
const PipedDownDialog({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -8,9 +7,8 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||||
import 'package:spotube/components/shared/image/universal_image.dart';
|
import 'package:spotube/components/shared/image/universal_image.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/provider/spotify_provider.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
|
|
||||||
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
class PlaylistAddTrackDialog extends HookConsumerWidget {
|
||||||
/// The id of the playlist this dialog was opened from
|
/// The id of the playlist this dialog was opened from
|
||||||
@ -19,33 +17,40 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
const PlaylistAddTrackDialog({
|
const PlaylistAddTrackDialog({
|
||||||
required this.tracks,
|
required this.tracks,
|
||||||
required this.openFromPlaylist,
|
required this.openFromPlaylist,
|
||||||
Key? key,
|
super.key,
|
||||||
}) : super(key: key);
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final ThemeData(:textTheme) = Theme.of(context);
|
final ThemeData(:textTheme) = Theme.of(context);
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final userPlaylists = ref.watch(favoritePlaylistsProvider);
|
||||||
final userPlaylists = useQueries.playlist.ofMineAll(ref);
|
final favoritePlaylistsNotifier =
|
||||||
|
ref.watch(favoritePlaylistsProvider.notifier);
|
||||||
|
|
||||||
final me = useQueries.user.me(ref);
|
final me = ref.watch(meProvider);
|
||||||
|
|
||||||
final filteredPlaylists = useMemoized(
|
final filteredPlaylists = useMemoized(
|
||||||
() =>
|
() =>
|
||||||
userPlaylists.data
|
userPlaylists.asData?.value.items
|
||||||
?.where(
|
.where(
|
||||||
(playlist) =>
|
(playlist) =>
|
||||||
playlist.owner?.id != null &&
|
playlist.owner?.id != null &&
|
||||||
playlist.owner!.id == me.data?.id &&
|
playlist.owner!.id == me.asData?.value.id &&
|
||||||
playlist.id != openFromPlaylist,
|
playlist.id != openFromPlaylist,
|
||||||
)
|
)
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
[userPlaylists.data, me.data?.id, openFromPlaylist],
|
[userPlaylists.asData?.value, me.asData?.value.id, openFromPlaylist],
|
||||||
);
|
);
|
||||||
|
|
||||||
final playlistsCheck = useState(<String, bool>{});
|
final playlistsCheck = useState(<String, bool>{});
|
||||||
final queryClient = useQueryClient();
|
|
||||||
|
useEffect(() {
|
||||||
|
if (userPlaylists.asData?.value != null) {
|
||||||
|
favoritePlaylistsNotifier.fetchAll();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [userPlaylists.asData?.value]);
|
||||||
|
|
||||||
Future<void> onAdd() async {
|
Future<void> onAdd() async {
|
||||||
final selectedPlaylists = playlistsCheck.value.entries
|
final selectedPlaylists = playlistsCheck.value.entries
|
||||||
@ -54,21 +59,12 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
selectedPlaylists.map(
|
selectedPlaylists.map(
|
||||||
(playlistId) => spotify.playlists.addTracks(
|
(playlistId) => favoritePlaylistsNotifier.addTracks(
|
||||||
tracks
|
playlistId,
|
||||||
.map(
|
tracks.map((e) => e.id!).toList(),
|
||||||
(track) => track.uri!,
|
),
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
playlistId),
|
|
||||||
),
|
),
|
||||||
).then((_) => Navigator.pop(context, true));
|
).then((_) => Navigator.pop(context, true));
|
||||||
|
|
||||||
await queryClient.refreshQueries(
|
|
||||||
selectedPlaylists
|
|
||||||
.map((playlistId) => "playlist-tracks/$playlistId")
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
@ -109,8 +105,7 @@ class PlaylistAddTrackDialog extends HookConsumerWidget {
|
|||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
secondary: CircleAvatar(
|
secondary: CircleAvatar(
|
||||||
backgroundImage: UniversalImage.imageProvider(
|
backgroundImage: UniversalImage.imageProvider(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
playlist.images.asUrlString(
|
||||||
playlist.images,
|
|
||||||
placeholder: ImagePlaceholder.collection,
|
placeholder: ImagePlaceholder.collection,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -8,8 +8,7 @@ final replaceDownloadedFileState = StateProvider<bool?>((ref) => null);
|
|||||||
|
|
||||||
class ReplaceDownloadedDialog extends ConsumerWidget {
|
class ReplaceDownloadedDialog extends ConsumerWidget {
|
||||||
final Track track;
|
final Track track;
|
||||||
const ReplaceDownloadedDialog({required this.track, Key? key})
|
const ReplaceDownloadedDialog({required this.track, super.key});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
|
|||||||
70
lib/components/shared/dialogs/select_device_dialog.dart
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
|
||||||
|
class SelectDeviceDialog extends HookConsumerWidget {
|
||||||
|
const SelectDeviceDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final isRemoteService = useState(false);
|
||||||
|
|
||||||
|
final connectClients = ref.watch(connectClientsProvider);
|
||||||
|
final remoteService = connectClients.asData!.value.resolvedService!;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("Choose the device:"),
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"There are multiple device connected.\n"
|
||||||
|
"Choose the device you want this action to take place",
|
||||||
|
),
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
title: Text(remoteService.name),
|
||||||
|
value: true,
|
||||||
|
groupValue: isRemoteService.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isRemoteService.value = value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile.adaptive(
|
||||||
|
title: const Text("This Device"),
|
||||||
|
value: false,
|
||||||
|
groupValue: isRemoteService.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
isRemoteService.value = !value!;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(isRemoteService.value);
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.select),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> showSelectDeviceDialog(BuildContext context, WidgetRef ref) async {
|
||||||
|
final connectClients = ref.read(connectClientsProvider);
|
||||||
|
|
||||||
|
if (connectClients.asData?.value.resolvedService == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isRemote = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const SelectDeviceDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return isRemote ?? false;
|
||||||
|
}
|
||||||