Merge branch 'master' into master

This commit is contained in:
Akshat Singh Kushwaha 2024-04-30 20:23:05 +05:30 committed by GitHub
commit 73fbccbdb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
483 changed files with 24674 additions and 14288 deletions

View File

@ -1,4 +1,4 @@
{ {
"flutterSdkVersion": "3.16.0", "flutterSdkVersion": "3.19.1",
"flavors": {} "flavors": {}
} }

View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -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
View 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(),",
");"
]
},
}

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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>
[![HackerNews](https://hackerbadge.vercel.app/api?id=39066136&type=dark)](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.

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/logos/songlink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View 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');
}
}

View File

@ -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,

View File

@ -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>

View File

@ -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'

View File

@ -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

View File

@ -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 = (

View File

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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",
),
)
],
);
} }

View File

@ -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(

View File

@ -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",

View File

@ -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()),
),
],
);
});

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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,

View File

@ -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!),
), ),

View 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");
},
),
),
],
),
);
}
}

View 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),
),
);
},
),
],
);
}
}

View File

@ -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();

View 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,
),
),
),
);
}
}

View File

@ -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,
), ),
); );
} }

View 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}"),
),
),
);
},
);
}
}

View File

@ -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),
),
],
),
],
),
), ),
), ),
), ),

View File

@ -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}",

View File

@ -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: [

View File

@ -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))

View File

@ -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,
); );
} }
} }

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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,

View File

@ -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,
);
},
);
}),
),
],
), ),
), ),
), ),

View File

@ -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(),
),
),
),
),
),
),
),
),
); );
} }
} }

View File

@ -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) {

View File

@ -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

View File

@ -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,
),
), ),
), ),
), ),

View File

@ -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),

View File

@ -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) {

View File

@ -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);
},
);
}),
), ),
), ),
], ],

View File

@ -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]);

View File

@ -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;
}
}, },
); );
}), }),

View File

@ -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,
), ),
), ),

View File

@ -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),
],
), ),
], ),
),
), ),
), ),
), );
), },
); );
} }
} }

View File

@ -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);
}, },

View File

@ -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);

View File

@ -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);
} }
}, },
), ),

View File

@ -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: () {

View File

@ -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(

View File

@ -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);
},
);
}),
) )
], ],
) ),
], ],
), ),
), ),

View File

@ -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);
},
), ),
], ],
), ),

View File

@ -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) {

View File

@ -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,

View File

@ -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) {

View File

@ -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

View File

@ -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;

View File

@ -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,
], ],

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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,
), ),
), ),

View File

@ -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) {

View 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;
}

Some files were not shown because too many files have changed in this diff Show More