mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'KRTirtho:master' into feature_duration_matching
This commit is contained in:
commit
cf1a8eff2a
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
enable-beta-ecosystems: true
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pub"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
15
.github/workflows/spotube-nightly.yml
vendored
15
.github/workflows/spotube-nightly.yml
vendored
@ -39,6 +39,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Spotube-Linux-Bundle
|
name: Spotube-Linux-Bundle
|
||||||
path: dist/
|
path: dist/
|
||||||
|
- name: Setup upterm session
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: lhotari/action-upterm@v1
|
||||||
|
with:
|
||||||
|
limit-access-to-actor: true
|
||||||
|
|
||||||
build_android:
|
build_android:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -63,6 +68,11 @@ jobs:
|
|||||||
name: Spotube-Android-Bundle
|
name: Spotube-Android-Bundle
|
||||||
path: |
|
path: |
|
||||||
build/Spotube-android-all-arch.apk
|
build/Spotube-android-all-arch.apk
|
||||||
|
- name: Setup upterm session
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: lhotari/action-upterm@v1
|
||||||
|
with:
|
||||||
|
limit-access-to-actor: true
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@ -109,3 +119,8 @@ jobs:
|
|||||||
name: Spotube-Macos-Bundle
|
name: Spotube-Macos-Bundle
|
||||||
path: |
|
path: |
|
||||||
build/Spotube-macos-x86_64.dmg
|
build/Spotube-macos-x86_64.dmg
|
||||||
|
- name: Setup upterm session
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: lhotari/action-upterm@v1
|
||||||
|
with:
|
||||||
|
limit-access-to-actor: true
|
||||||
|
103
.github/workflows/spotube-release.yml
vendored
103
.github/workflows/spotube-release.yml
vendored
@ -1,10 +1,8 @@
|
|||||||
name: Spotube Release
|
name: Spotube Release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types:
|
||||||
tag:
|
- published
|
||||||
description: The tag to release
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish_chocolatey:
|
publish_chocolatey:
|
||||||
@ -16,18 +14,17 @@ jobs:
|
|||||||
repository: KRTirtho/flutter_distributor
|
repository: KRTirtho/flutter_distributor
|
||||||
ref: deb-implementation
|
ref: deb-implementation
|
||||||
path: build/flutter_distributor
|
path: build/flutter_distributor
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# # Optionally strip `v` prefix
|
strip_v: true
|
||||||
# strip_v: true
|
|
||||||
# Replace Version in files
|
# Replace Version in files
|
||||||
- run: |
|
- run: |
|
||||||
choco install sed make -y
|
choco install sed make -y
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.tag }}/" windows/runner/Runner.rc
|
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" windows/runner/Runner.rc
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.tag }}/" choco-struct/tools/VERIFICATION.txt
|
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" choco-struct/tools/VERIFICATION.txt
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.tag }}/" choco-struct/spotube.nuspec
|
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" choco-struct/spotube.nuspec
|
||||||
|
|
||||||
# Build Windows Executable
|
# Build Windows Executable
|
||||||
- uses: subosito/flutter-action@v2.2.0
|
- uses: subosito/flutter-action@v2.2.0
|
||||||
@ -67,11 +64,11 @@ jobs:
|
|||||||
runs-on: macos-11
|
runs-on: macos-11
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# id: tag
|
id: tag
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
@ -82,23 +79,23 @@ jobs:
|
|||||||
- run: du -sh build/macos/Build/Products/Release/spotube.app
|
- run: du -sh build/macos/Build/Products/Release/spotube.app
|
||||||
- run: npm install -g appdmg
|
- run: npm install -g appdmg
|
||||||
# using a versioned path for compatibility in gensums
|
# using a versioned path for compatibility in gensums
|
||||||
- run: mkdir -p build/${{ github.event.inputs.tag }}
|
- run: mkdir -p build/${{ steps.tag.outputs.tag }}
|
||||||
- run: appdmg appdmg.json build/${{ github.event.inputs.tag }}/Spotube-macos-x86_64.dmg
|
- run: appdmg appdmg.json build/${{ steps.tag.outputs.tag }}/Spotube-macos-x86_64.dmg
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: Spotube-Macos-Bundle
|
name: Spotube-Macos-Bundle
|
||||||
path: |
|
path: |
|
||||||
build/${{ github.event.inputs.tag }}/Spotube-macos-x86_64.dmg
|
build/${{ steps.tag.outputs.tag }}/Spotube-macos-x86_64.dmg
|
||||||
|
|
||||||
publish_linux:
|
publish_linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
@ -114,7 +111,7 @@ jobs:
|
|||||||
mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
||||||
# replacing & adding new release version with older version
|
# replacing & adding new release version with older version
|
||||||
- run: |
|
- run: |
|
||||||
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${{ github.event.inputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
|
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${{ steps.tag.outputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
flutter config --enable-linux-desktop
|
flutter config --enable-linux-desktop
|
||||||
@ -136,11 +133,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
cache: true
|
cache: true
|
||||||
@ -187,11 +184,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: Spotube-Android-Bundle
|
name: Spotube-Android-Bundle
|
||||||
path: ./Spotube-Android-Bundle
|
path: ./Spotube-Android-Bundle
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- run: sudo apt-get install tree -y
|
- run: sudo apt-get install tree -y
|
||||||
# generating checksums for all the binary
|
# generating checksums for all the binary
|
||||||
- run: |
|
- run: |
|
||||||
@ -209,7 +206,7 @@ jobs:
|
|||||||
- uses: ncipollo/release-action@v1
|
- uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: v${{ github.event.inputs.tag }}
|
tag: v${{ steps.tag.outputs.tag }}
|
||||||
omitBodyDuringUpdate: true
|
omitBodyDuringUpdate: true
|
||||||
omitNameDuringUpdate: true
|
omitNameDuringUpdate: true
|
||||||
omitPrereleaseDuringUpdate: true
|
omitPrereleaseDuringUpdate: true
|
||||||
@ -236,17 +233,17 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
path: spotube
|
path: spotube
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- run: |
|
- run: |
|
||||||
python3 spotube/scripts/update_flathub_version.py ${{ github.event.inputs.tag }}
|
python3 spotube/scripts/update_flathub_version.py ${{ steps.tag.outputs.tag }}
|
||||||
rm -rf spotube
|
rm -rf spotube
|
||||||
- uses: EndBug/add-and-commit@v9
|
- uses: EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
message: v${{ github.event.inputs.tag }} Update
|
message: v${{ steps.tag.outputs.tag }} Update
|
||||||
push: origin master
|
push: origin master
|
||||||
|
|
||||||
publish_aur:
|
publish_aur:
|
||||||
@ -254,17 +251,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
# - name: Get latest tag
|
- name: Get latest tag
|
||||||
# id: tag
|
id: tag
|
||||||
# uses: dawidd6/action-get-tag@v1
|
uses: dawidd6/action-get-tag@v1
|
||||||
# with:
|
with:
|
||||||
# strip_v: true
|
strip_v: true
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Spotube-Linux-Bundle
|
name: Spotube-Linux-Bundle
|
||||||
path: ./Spotube-Linux-Bundle
|
path: ./Spotube-Linux-Bundle
|
||||||
- run: |
|
- run: |
|
||||||
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.tag }}/" aur-struct/PKGBUILD
|
sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" aur-struct/PKGBUILD
|
||||||
sed -i "s/%{{PKGREL}}%/1/" aur-struct/PKGBUILD
|
sed -i "s/%{{PKGREL}}%/1/" aur-struct/PKGBUILD
|
||||||
sed -i "s/%{{LINUX_MD5}}%/`md5sum Spotube-Linux-Bundle/Spotube-linux-x86_64.tar.xz | awk '{print $1}'`/" aur-struct/PKGBUILD
|
sed -i "s/%{{LINUX_MD5}}%/`md5sum Spotube-Linux-Bundle/Spotube-linux-x86_64.tar.xz | awk '{print $1}'`/" aur-struct/PKGBUILD
|
||||||
- uses: KSXGitHub/github-actions-deploy-aur@v2.2.5
|
- uses: KSXGitHub/github-actions-deploy-aur@v2.2.5
|
||||||
@ -274,4 +271,4 @@ jobs:
|
|||||||
commit_username: ${{ secrets.AUR_USERNAME }}
|
commit_username: ${{ secrets.AUR_USERNAME }}
|
||||||
commit_email: ${{ secrets.AUR_EMAIL }}
|
commit_email: ${{ secrets.AUR_EMAIL }}
|
||||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
commit_message: Updated to v${{ github.event.inputs.tag }}
|
commit_message: Updated to v${{ steps.tag.outputs.tag }}
|
||||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@ -1,3 +1,59 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## [2.4.1](https://github.com/KRTirtho/spotube/compare/v2.4.0...v2.4.1) (2022-09-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add macos audio metadata tags support ([5866b0f](https://github.com/KRTirtho/spotube/commit/5866b0fcd661cf32060bb1485ea81634fbb9b90a))
|
||||||
|
* remove macos bounds for reading and writing audio metadata ([16064f6](https://github.com/KRTirtho/spotube/commit/16064f68e882b091401ace4b895e387f46635800))
|
||||||
|
* **search:** horizontal swipe scroll support for Desktop platform ([d5ff927](https://github.com/KRTirtho/spotube/commit/d5ff927c7273b6e72c5d775ee777f2cbd0d6d05c))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **artist-page:** SpotubeMarqueeText used in ArtistCard crashes the app ([4279541](https://github.com/KRTirtho/spotube/commit/427954150ab65b250e79fc844fc864abff5b6972))
|
||||||
|
* **layout:** Fix adaptive UI not working correctly by providing a overriding option ([8c7adde](https://github.com/KRTirtho/spotube/commit/8c7adde890105e0267b71994b7928277f84553e5))
|
||||||
|
* **local-track:** throwing exception when downloadLocation is empty ([1a3556d](https://github.com/KRTirtho/spotube/commit/1a3556d39e8473cadb6143192c48465dc6485599))
|
||||||
|
|
||||||
|
## [2.4.0](https://github.com/KRTirtho/spotube/compare/v2.3.0...v2.4.0) (2022-09-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Ability to change download location added ([816707c](https://github.com/KRTirtho/spotube/commit/816707c643f8d60d25bc08fd4c8005daa2ba9e63))
|
||||||
|
* add download multi tracks support for mobile platform ([0476bf7](https://github.com/KRTirtho/spotube/commit/0476bf7ceece034a927d1df6099d8b33036f8a9b))
|
||||||
|
* add download queue for desktop & initial playlist download support ([08f913e](https://github.com/KRTirtho/spotube/commit/08f913e9761d0f5c447af9dfb6eedb44b675498c))
|
||||||
|
* add download tab on library ([8d77b69](https://github.com/KRTirtho/spotube/commit/8d77b6900a81aab020e19397e788964b0ac499ff))
|
||||||
|
* add web support although nothing works just as expected ([2818ed5](https://github.com/KRTirtho/spotube/commit/2818ed5c9dadb9185a52762599c1dd0acd81e6bf))
|
||||||
|
* **broken:** Broken Warning! Initial Local Audio Player ([c3bf511](https://github.com/KRTirtho/spotube/commit/c3bf5119ebb7c17e8c32f149598508674b0acd39))
|
||||||
|
* **download:** track table view multi select improvement, tap to play track support, existing track replace confirmation dialog and bulk download confirmation dialog ([e217553](https://github.com/KRTirtho/spotube/commit/e21755322f2cd5f1fba00c5c8cd5c5d1f79e459d))
|
||||||
|
* **local-tracks:** complete support for local tracks ([e206f16](https://github.com/KRTirtho/spotube/commit/e206f16723ac989ad58006c1b3c90c6691d8cab3))
|
||||||
|
* **mpris:** MPRIS metadata are now updated in realtime ([d9addcd](https://github.com/KRTirtho/spotube/commit/d9addcda8e9562803bd73016148fab22560ee050))
|
||||||
|
* **playback:** add repeat track support [#166](https://github.com/KRTirtho/spotube/issues/166) ([cae9993](https://github.com/KRTirtho/spotube/commit/cae99934299bd197c68f626d6c10158d449770b9))
|
||||||
|
* **synced-lyrics:** animated active text size ([531fae6](https://github.com/KRTirtho/spotube/commit/531fae64f94b21551a7a0da363a9ab0d44f5d3b1))
|
||||||
|
* **ui:** adaptive TrackTile actions & Setting ListTile ([615d5ce](https://github.com/KRTirtho/spotube/commit/615d5ce901eb0512e84a120b7309c9053238ee36))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **adaptive-list-tile:** dialog content not updating when content has changed ([a1d4230](https://github.com/KRTirtho/spotube/commit/a1d423090c854ebe319a0fa03fd6e5c4007b1387))
|
||||||
|
* album & playlist card, player view and album view play button logic ([55852bd](https://github.com/KRTirtho/spotube/commit/55852bd15bc709d61fbba8cbea01ceca791d154c))
|
||||||
|
* **docs:** indentions ([4a291d5](https://github.com/KRTirtho/spotube/commit/4a291d5f20dabe68f3ed64071624dcbed8327329))
|
||||||
|
* **downloader:** downloaded track is corrupted for tagging ([2ab1fba](https://github.com/KRTirtho/spotube/commit/2ab1fba3d64147e3c5cf34756dce1cf6046d410a))
|
||||||
|
* **downloader:** flutter downloader exception on desktop platform and too much width of TrackTile index no. ([d668760](https://github.com/KRTirtho/spotube/commit/d6687603d148ad936530cca4d09e128a59b79b19))
|
||||||
|
* dropped flutter_downloader deps due to slow download speed and UserDownloads not showing for anonymous ([307a8e2](https://github.com/KRTirtho/spotube/commit/307a8e21df1e39123a1dca4c1b063eab50359581))
|
||||||
|
* flutter_downloader manifest configuration breaking android support ([f3a0f78](https://github.com/KRTirtho/spotube/commit/f3a0f78fb92ff7ee38b5a9ef9954575d4282f954))
|
||||||
|
* login screen not using safearea and no dialog bg-color found on light mode in AdaptivePopupMenuButton ([92bc611](https://github.com/KRTirtho/spotube/commit/92bc611c5e901dcabf34086be9287ac20317259a))
|
||||||
|
* **performance:** always running marquee text causes high GPU usage [#175](https://github.com/KRTirtho/spotube/issues/175) and UserArtist overflow on smaller displays ([a23ce61](https://github.com/KRTirtho/spotube/commit/a23ce614467b4297f495b824f0958ff07c21ae92))
|
||||||
|
* **playback:** shuffle button sometimes gets stuck and stops working [#183](https://github.com/KRTirtho/spotube/issues/183) ([4240433](https://github.com/KRTirtho/spotube/commit/4240433e3dde6ab948d2674e07e41c27c1f6eac8))
|
||||||
|
* **player-overlay:** flickering when a track is changed or navigated to another page ([e48b67c](https://github.com/KRTirtho/spotube/commit/e48b67cd47ae54ad9268aead268e444836a67b0d))
|
||||||
|
* **sidebar:** user image url ([747efc6](https://github.com/KRTirtho/spotube/commit/747efc6ee66bc6c7c917cc02bd134968a0781701))
|
||||||
|
* **synced-lyrics:** active lyrics contrast ratio ([aba1ba9](https://github.com/KRTirtho/spotube/commit/aba1ba932592923720a36395c057f78820dafecf))
|
||||||
|
* tabbar overflow in small screen, artist card too small title and synced lyrics contrast increased ([585de8c](https://github.com/KRTirtho/spotube/commit/585de8c1def9750826568317109b242a5e18f28c))
|
||||||
|
|
||||||
# v2.3.0
|
# v2.3.0
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
@ -21,7 +21,10 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
final int marginH =
|
final int marginH =
|
||||||
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
|
||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(album.images),
|
imageUrl: TypeConversionUtils.image_X_UrlString(
|
||||||
|
album.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
||||||
isLoading: playback.status == PlaybackStatus.loading &&
|
isLoading: playback.status == PlaybackStatus.loading &&
|
||||||
|
@ -27,7 +27,10 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: album.id!,
|
id: album.id!,
|
||||||
name: album.name!,
|
name: album.name!,
|
||||||
thumbnail: TypeConversionUtils.image_X_UrlString(album.images),
|
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||||
|
album.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
@ -50,7 +53,10 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||||
|
|
||||||
final albumArt = useMemoized(
|
final albumArt = useMemoized(
|
||||||
() => TypeConversionUtils.image_X_UrlString(album.images),
|
() => TypeConversionUtils.image_X_UrlString(
|
||||||
|
album.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
[album.images]);
|
[album.images]);
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
@ -69,6 +75,14 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
onPlay: ([track]) {
|
onPlay: ([track]) {
|
||||||
if (tracksSnapshot.asData?.value != null) {
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
if (!isAlbumPlaying) {
|
if (!isAlbumPlaying) {
|
||||||
|
playPlaylist(
|
||||||
|
playback,
|
||||||
|
tracksSnapshot.asData!.value
|
||||||
|
.map((track) =>
|
||||||
|
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
} else if (isAlbumPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
playback,
|
||||||
tracksSnapshot.asData!.value
|
tracksSnapshot.asData!.value
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
||||||
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class ArtistCard extends StatelessWidget {
|
class ArtistCard extends StatelessWidget {
|
||||||
final Artist artist;
|
final Artist artist;
|
||||||
@ -11,11 +12,12 @@ class ArtistCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final backgroundImage = CachedNetworkImageProvider((artist
|
final backgroundImage = UniversalImage.imageProvider(
|
||||||
.images?.isNotEmpty ??
|
TypeConversionUtils.image_X_UrlString(
|
||||||
false)
|
artist.images,
|
||||||
? artist.images!.first.url!
|
placeholder: ImagePlaceholder.artist,
|
||||||
: "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6");
|
),
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 240,
|
height: 240,
|
||||||
width: 200,
|
width: 200,
|
||||||
@ -32,32 +34,55 @@ class ArtistCard extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 3),
|
offset: const Offset(0, 3),
|
||||||
spreadRadius: 5,
|
spreadRadius: 5,
|
||||||
color: Theme.of(context).shadowColor)
|
color: Theme.of(context).shadowColor,
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
Stack(
|
||||||
maxRadius: 80,
|
children: [
|
||||||
minRadius: 20,
|
CircleAvatar(
|
||||||
backgroundImage: backgroundImage,
|
maxRadius: 80,
|
||||||
|
minRadius: 20,
|
||||||
|
backgroundImage: backgroundImage,
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
borderRadius: BorderRadius.circular(50)),
|
||||||
|
child: const Text(
|
||||||
|
"Artist",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SpotubeMarqueeText(
|
AutoSizeText(
|
||||||
text: artist.name!,
|
artist.name!,
|
||||||
|
maxLines: 2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
isHovering: isHovering,
|
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
"Artist",
|
|
||||||
style: Theme.of(context).textTheme.subtitle1,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
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';
|
||||||
@ -10,6 +9,7 @@ import 'package:spotube/components/Artist/ArtistCard.dart';
|
|||||||
import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart';
|
import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||||
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
@ -78,8 +78,11 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
const SizedBox(width: 50),
|
const SizedBox(width: 50),
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: avatarWidth,
|
radius: avatarWidth,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: UniversalImage.imageProvider(
|
||||||
TypeConversionUtils.image_X_UrlString(data.images),
|
TypeConversionUtils.image_X_UrlString(
|
||||||
|
data.images,
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@ -193,7 +196,9 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
id: data.id!,
|
id: data.id!,
|
||||||
name: "${data.name!} To Tracks",
|
name: "${data.name!} To Tracks",
|
||||||
thumbnail: TypeConversionUtils.image_X_UrlString(
|
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||||
data.images),
|
data.images,
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
@ -233,10 +238,10 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
String? thumbnailUrl =
|
String? thumbnailUrl =
|
||||||
TypeConversionUtils.image_X_UrlString(
|
TypeConversionUtils.image_X_UrlString(
|
||||||
track.value.album?.images,
|
track.value.album?.images,
|
||||||
index:
|
index: (track.value.album?.images?.length ?? 1) - 1,
|
||||||
(track.value.album?.images?.length ?? 1) -
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
1);
|
);
|
||||||
return TrackTile(
|
return TrackTile(
|
||||||
playback,
|
playback,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
@ -12,7 +12,7 @@ import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
|
|||||||
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
|
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
|
||||||
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
|
import 'package:spotube/components/Lyrics/SyncedLyrics.dart';
|
||||||
import 'package:spotube/components/Search/Search.dart';
|
import 'package:spotube/components/Search/Search.dart';
|
||||||
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Player/Player.dart';
|
import 'package:spotube/components/Player/Player.dart';
|
||||||
import 'package:spotube/components/Library/UserLibrary.dart';
|
import 'package:spotube/components/Library/UserLibrary.dart';
|
||||||
@ -43,13 +43,14 @@ class Home extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final int titleBarDragMaxWidth = useBreakpointValue(
|
final double titleBarWidth = useBreakpointValue(
|
||||||
md: 80,
|
sm: 0.0,
|
||||||
lg: 256,
|
md: 80.0,
|
||||||
sm: 0,
|
lg: 256.0,
|
||||||
xl: 256,
|
xl: 256.0,
|
||||||
xxl: 256,
|
xxl: 256.0,
|
||||||
);
|
);
|
||||||
|
final extended = ref.watch(sidebarExtendedStateProvider);
|
||||||
final _selectedIndex = useState(0);
|
final _selectedIndex = useState(0);
|
||||||
_onSelectedIndexChanged(int index) => _selectedIndex.value = index;
|
_onSelectedIndexChanged(int index) => _selectedIndex.value = index;
|
||||||
|
|
||||||
@ -82,7 +83,9 @@ class Home extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: titleBarDragMaxWidth.toDouble(),
|
maxWidth: extended == null
|
||||||
|
? titleBarWidth
|
||||||
|
: (extended ? 256 : 80),
|
||||||
),
|
),
|
||||||
color: Theme.of(context).navigationRailTheme.backgroundColor,
|
color: Theme.of(context).navigationRailTheme.backgroundColor,
|
||||||
child: MoveWindow(),
|
child: MoveWindow(),
|
||||||
@ -111,6 +114,10 @@ class Home extends HookConsumerWidget {
|
|||||||
}, [backgroundColor]);
|
}, [backgroundColor]);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
bottomNavigationBar: SpotubeNavigationBar(
|
||||||
|
selectedIndex: _selectedIndex.value,
|
||||||
|
onSelectedIndexChanged: _onSelectedIndexChanged,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_selectedIndex.value != 3)
|
if (_selectedIndex.value != 3)
|
||||||
@ -178,10 +185,6 @@ class Home extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
// player itself
|
// player itself
|
||||||
Player(),
|
Player(),
|
||||||
SpotubeNavigationBar(
|
|
||||||
selectedIndex: _selectedIndex.value,
|
|
||||||
onSelectedIndexChanged: _onSelectedIndexChanged,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
import 'package:badges/badges.dart';
|
import 'package:badges/badges.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
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:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
import 'package:spotube/provider/Downloader.dart';
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
import 'package:spotube/provider/UserPreferences.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/type_conversion_utils.dart';
|
||||||
|
|
||||||
|
final sidebarExtendedStateProvider = StateProvider<bool?>((ref) => null);
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
final void Function(int) onSelectedIndexChanged;
|
final void Function(int) onSelectedIndexChanged;
|
||||||
@ -45,16 +47,15 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final downloadCount = ref.watch(
|
final downloadCount = ref.watch(
|
||||||
downloaderProvider.select((s) => s.currentlyRunning),
|
downloaderProvider.select((s) => s.currentlyRunning),
|
||||||
);
|
);
|
||||||
|
final forceExtended = ref.watch(sidebarExtendedStateProvider);
|
||||||
final int titleBarDragMaxWidth = useBreakpointValue(
|
|
||||||
md: 80,
|
|
||||||
lg: 256,
|
|
||||||
sm: 0,
|
|
||||||
xl: 256,
|
|
||||||
xxl: 256,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
if (forceExtended != null) {
|
||||||
|
if (extended.value != forceExtended) {
|
||||||
|
extended.value = forceExtended;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (breakpoints.isMd && extended.value) {
|
if (breakpoints.isMd && extended.value) {
|
||||||
extended.value = false;
|
extended.value = false;
|
||||||
} else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) &&
|
} else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) &&
|
||||||
@ -64,7 +65,17 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (breakpoints.isSm) return Container();
|
final layoutMode =
|
||||||
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
|
if (layoutMode == LayoutMode.compact ||
|
||||||
|
(breakpoints.isSm && layoutMode == LayoutMode.adaptive)) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleExtended() =>
|
||||||
|
ref.read(sidebarExtendedStateProvider.notifier).state =
|
||||||
|
!(forceExtended ?? extended.value);
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Material(
|
child: Material(
|
||||||
@ -75,11 +86,11 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
if (selectedIndex == 3 && kIsDesktop)
|
if (selectedIndex == 3 && kIsDesktop)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: appWindow.titleBarHeight,
|
height: appWindow.titleBarHeight,
|
||||||
width: titleBarDragMaxWidth.toDouble(),
|
width: extended.value ? 256 : 80,
|
||||||
child: MoveWindow(),
|
child: MoveWindow(),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 15),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: (extended.value)
|
child: (extended.value)
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
@ -87,11 +98,25 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 10,
|
width: 10,
|
||||||
),
|
),
|
||||||
Text("Spotube",
|
Text(
|
||||||
style: Theme.of(context).textTheme.headline4),
|
"Spotube",
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.menu_rounded),
|
||||||
|
onPressed: toggleExtended,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: _buildSmallLogo(),
|
: Column(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.menu_rounded),
|
||||||
|
onPressed: toggleExtended,
|
||||||
|
),
|
||||||
|
_buildSmallLogo(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: NavigationRail(
|
child: NavigationRail(
|
||||||
@ -129,14 +154,16 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: titleBarDragMaxWidth.toDouble(),
|
width: extended.value ? 256 : 80,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final data = meSnapshot.asData?.value;
|
final data = meSnapshot.asData?.value;
|
||||||
|
|
||||||
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
final avatarImg = TypeConversionUtils.image_X_UrlString(
|
||||||
data?.images,
|
data?.images,
|
||||||
index: (data?.images?.length ?? 1) - 1);
|
index: (data?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
);
|
||||||
if (extended.value) {
|
if (extended.value) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -155,7 +182,8 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
CachedNetworkImageProvider(avatarImg),
|
UniversalImage.imageProvider(
|
||||||
|
avatarImg),
|
||||||
onBackgroundImageError:
|
onBackgroundImageError:
|
||||||
(exception, stackTrace) =>
|
(exception, stackTrace) =>
|
||||||
Image.asset(
|
Image.asset(
|
||||||
@ -193,7 +221,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
onTap: () => goToSettings(context),
|
onTap: () => goToSettings(context),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
backgroundImage:
|
backgroundImage:
|
||||||
CachedNetworkImageProvider(avatarImg),
|
UniversalImage.imageProvider(avatarImg),
|
||||||
onBackgroundImageError: (exception, stackTrace) =>
|
onBackgroundImageError: (exception, stackTrace) =>
|
||||||
Image.asset(
|
Image.asset(
|
||||||
"assets/user-placeholder.png",
|
"assets/user-placeholder.png",
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import 'package:badges/badges.dart';
|
import 'package:badges/badges.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/Home/Sidebar.dart';
|
import 'package:spotube/components/Home/Sidebar.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/Downloader.dart';
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
|
||||||
class SpotubeNavigationBar extends HookConsumerWidget {
|
class SpotubeNavigationBar extends HookConsumerWidget {
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
@ -23,8 +23,12 @@ class SpotubeNavigationBar extends HookConsumerWidget {
|
|||||||
downloaderProvider.select((s) => s.currentlyRunning),
|
downloaderProvider.select((s) => s.currentlyRunning),
|
||||||
);
|
);
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
final layoutMode =
|
||||||
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
if (breakpoint.isMoreThan(Breakpoints.sm)) return Container();
|
if (layoutMode == LayoutMode.extended ||
|
||||||
|
(breakpoint.isMoreThan(Breakpoints.sm) &&
|
||||||
|
layoutMode == LayoutMode.adaptive)) return const SizedBox();
|
||||||
return NavigationBar(
|
return NavigationBar(
|
||||||
destinations: [
|
destinations: [
|
||||||
...sidebarTileList.map(
|
...sidebarTileList.map(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
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:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
import 'package:spotube/provider/Downloader.dart';
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
@ -53,11 +54,12 @@ class UserDownloads extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: CachedNetworkImage(
|
child: UniversalImage(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(
|
path: TypeConversionUtils.image_X_UrlString(
|
||||||
track.album?.images,
|
track.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dart_tags/dart_tags.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:mime/mime.dart';
|
import 'package:mime/mime.dart';
|
||||||
import 'package:mp3_info/mp3_info.dart';
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -18,14 +15,12 @@ import 'package:spotube/provider/Playback.dart';
|
|||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
import 'package:id3/id3.dart';
|
|
||||||
|
|
||||||
final tagProcessor = TagProcessor();
|
|
||||||
|
|
||||||
const supportedAudioTypes = [
|
const supportedAudioTypes = [
|
||||||
"audio/webm",
|
"audio/webm",
|
||||||
"audio/ogg",
|
"audio/ogg",
|
||||||
"audio/mpeg",
|
"audio/mpeg",
|
||||||
|
"audio/mp4",
|
||||||
"audio/opus",
|
"audio/opus",
|
||||||
"audio/wav",
|
"audio/wav",
|
||||||
"audio/aac",
|
"audio/aac",
|
||||||
@ -40,9 +35,11 @@ const imgMimeToExt = {
|
|||||||
|
|
||||||
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
||||||
try {
|
try {
|
||||||
final downloadDir = Directory(
|
final downloadLocation = ref.watch(
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)),
|
userPreferencesProvider.select((s) => s.downloadLocation),
|
||||||
);
|
);
|
||||||
|
if (downloadLocation.isEmpty) return [];
|
||||||
|
final downloadDir = Directory(downloadLocation);
|
||||||
if (!await downloadDir.exists()) {
|
if (!await downloadDir.exists()) {
|
||||||
await downloadDir.create(recursive: true);
|
await downloadDir.create(recursive: true);
|
||||||
return [];
|
return [];
|
||||||
@ -56,70 +53,39 @@ final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
|||||||
}).map(
|
}).map(
|
||||||
(f) async {
|
(f) async {
|
||||||
try {
|
try {
|
||||||
final bytes = f.readAsBytes();
|
final metadata = await MetadataGod.getMetadata(f);
|
||||||
final mp3Instance = MP3Instance(await bytes);
|
|
||||||
|
|
||||||
bool isParsed = false;
|
final imageFile = File(join(
|
||||||
try {
|
(await getTemporaryDirectory()).path,
|
||||||
isParsed = mp3Instance.parseTagsSync();
|
"spotube",
|
||||||
} catch (e, stack) {
|
basenameWithoutExtension(f.path) +
|
||||||
getLogger(MP3Instance).e("[parseTagsSync]", e, stack);
|
imgMimeToExt[metadata?.picture?.mimeType ?? "image/jpeg"]!,
|
||||||
}
|
));
|
||||||
|
if (!await imageFile.exists() && metadata?.picture != null) {
|
||||||
final imageFile = isParsed
|
|
||||||
? File(join(
|
|
||||||
(await getTemporaryDirectory()).path,
|
|
||||||
"spotube",
|
|
||||||
basenameWithoutExtension(f.path) +
|
|
||||||
imgMimeToExt[mp3Instance.metaTags["APIC"]?["mime"] ??
|
|
||||||
"image/jpeg"]!,
|
|
||||||
))
|
|
||||||
: null;
|
|
||||||
if (imageFile != null &&
|
|
||||||
!await imageFile.exists() &&
|
|
||||||
mp3Instance.metaTags["APIC"]?["base64"] != null) {
|
|
||||||
await imageFile.create(recursive: true);
|
await imageFile.create(recursive: true);
|
||||||
await imageFile.writeAsBytes(
|
await imageFile.writeAsBytes(
|
||||||
base64Decode(
|
metadata?.picture?.data ?? [],
|
||||||
mp3Instance.metaTags["APIC"]["base64"],
|
|
||||||
),
|
|
||||||
mode: FileMode.writeOnly,
|
mode: FileMode.writeOnly,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Duration duration;
|
|
||||||
try {
|
|
||||||
duration = MP3Processor.fromBytes(await bytes).duration;
|
|
||||||
} catch (e, stack) {
|
|
||||||
getLogger(MP3Processor).e("[Parsing Mp3]", e, stack);
|
|
||||||
duration = Duration.zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
final metadata = await tagProcessor.getTagsFromByteArray(bytes);
|
return {"metadata": metadata, "file": f, "art": imageFile.path};
|
||||||
return {
|
|
||||||
"metadata": metadata,
|
|
||||||
"file": f,
|
|
||||||
"art": imageFile?.path,
|
|
||||||
"duration": duration,
|
|
||||||
};
|
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
|
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
|
||||||
return {
|
return {};
|
||||||
"metadata": <Tag>[],
|
|
||||||
"file": f,
|
|
||||||
"duration": Duration.zero,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
));
|
))
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final tracks = filesWithMetadata
|
final tracks = filesWithMetadata
|
||||||
.map(
|
.map(
|
||||||
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
||||||
fileWithMetadata["metadata"] as List<Tag>,
|
fileWithMetadata["file"],
|
||||||
fileWithMetadata["file"] as File,
|
metadata: fileWithMetadata["metadata"],
|
||||||
fileWithMetadata["duration"] as Duration,
|
art: fileWithMetadata["art"],
|
||||||
fileWithMetadata["art"] as String?,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
@ -144,7 +110,10 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: "local",
|
id: "local",
|
||||||
name: "Local Tracks",
|
name: "Local Tracks",
|
||||||
thumbnail: TypeConversionUtils.image_X_UrlString(null),
|
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||||
|
null,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
),
|
),
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
@ -217,17 +186,11 @@ class UserLocalTracks extends HookConsumerWidget {
|
|||||||
: "assets/album-placeholder.png",
|
: "assets/album-placeholder.png",
|
||||||
isLocal: true,
|
isLocal: true,
|
||||||
onTrackPlayButtonPressed: (currentTrack) {
|
onTrackPlayButtonPressed: (currentTrack) {
|
||||||
if (tracks.isNotEmpty) {
|
return playLocalTracks(
|
||||||
if (!isPlaylistPlaying) {
|
playback,
|
||||||
playLocalTracks(
|
tracks,
|
||||||
playback,
|
currentTrack: track,
|
||||||
tracks,
|
);
|
||||||
currentTrack: track,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
playback.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -62,6 +62,16 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (clientSecretController.text.isEmpty ||
|
||||||
|
clientIdController.text.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Please fill in all fields"),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await handleLogin(authState);
|
await handleLogin(authState);
|
||||||
},
|
},
|
||||||
child: const Text("Submit"),
|
child: const Text("Submit"),
|
||||||
|
@ -112,6 +112,7 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
() => TypeConversionUtils.image_X_UrlString(
|
() => TypeConversionUtils.image_X_UrlString(
|
||||||
playback.track?.album?.images,
|
playback.track?.album?.images,
|
||||||
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[playback.track?.album?.images],
|
[playback.track?.album?.images],
|
||||||
);
|
);
|
||||||
@ -221,22 +222,23 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
: Center(
|
: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: AutoSizeText(
|
child: AnimatedDefaultTextStyle(
|
||||||
lyricSlice.text,
|
duration: const Duration(
|
||||||
maxLines: 2,
|
milliseconds: 250),
|
||||||
style: Theme.of(context)
|
style: TextStyle(
|
||||||
.textTheme
|
color: isActive
|
||||||
.headline4
|
? Colors.white
|
||||||
?.copyWith(
|
: palette.bodyTextColor,
|
||||||
color: isActive
|
fontWeight: isActive
|
||||||
? Colors.white
|
? FontWeight.bold
|
||||||
: palette.bodyTextColor,
|
: FontWeight.normal,
|
||||||
// indicating the active state of that lyric slice
|
fontSize: isActive ? 30 : 26,
|
||||||
fontWeight: isActive
|
),
|
||||||
? FontWeight.bold
|
child: Text(
|
||||||
: null,
|
lyricSlice.text,
|
||||||
),
|
maxLines: 2,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -8,6 +8,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';
|
|||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class Player extends HookConsumerWidget {
|
class Player extends HookConsumerWidget {
|
||||||
@ -17,6 +18,8 @@ class Player extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
|
final layoutMode =
|
||||||
|
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
@ -25,6 +28,7 @@ class Player extends HookConsumerWidget {
|
|||||||
? TypeConversionUtils.image_X_UrlString(
|
? TypeConversionUtils.image_X_UrlString(
|
||||||
playback.track?.album?.images,
|
playback.track?.album?.images,
|
||||||
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
index: (playback.track?.album?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
)
|
)
|
||||||
: "assets/album-placeholder.png",
|
: "assets/album-placeholder.png",
|
||||||
[playback.track?.album?.images],
|
[playback.track?.album?.images],
|
||||||
@ -50,7 +54,9 @@ class Player extends HookConsumerWidget {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((time) {
|
WidgetsBinding.instance.addPostFrameCallback((time) {
|
||||||
// clearing the overlay-entry as passing the already available
|
// clearing the overlay-entry as passing the already available
|
||||||
// entry will result in splashing while resizing the window
|
// entry will result in splashing while resizing the window
|
||||||
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
if ((layoutMode == LayoutMode.compact ||
|
||||||
|
(breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
||||||
|
layoutMode == LayoutMode.adaptive)) &&
|
||||||
entryRef.value == null &&
|
entryRef.value == null &&
|
||||||
playback.track != null) {
|
playback.track != null) {
|
||||||
entryRef.value = OverlayEntry(
|
entryRef.value = OverlayEntry(
|
||||||
@ -74,11 +80,13 @@ class Player extends HookConsumerWidget {
|
|||||||
return () {
|
return () {
|
||||||
disposeOverlay();
|
disposeOverlay();
|
||||||
};
|
};
|
||||||
}, [breakpoint, playback.track]);
|
}, [breakpoint, playback.track, layoutMode]);
|
||||||
|
|
||||||
// returning an empty non spacious Container as the overlay will take
|
// returning an empty non spacious Container as the overlay will take
|
||||||
// place in the global overlay stack aka [_entries]
|
// place in the global overlay stack aka [_entries]
|
||||||
if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) {
|
if (layoutMode == LayoutMode.compact ||
|
||||||
|
(breakpoint.isLessThanOrEqualTo(Breakpoints.md) &&
|
||||||
|
layoutMode == LayoutMode.adaptive)) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/Library/UserLocalTracks.dart';
|
||||||
import 'package:spotube/components/Player/PlayerQueue.dart';
|
import 'package:spotube/components/Player/PlayerQueue.dart';
|
||||||
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
|
import 'package:spotube/provider/Downloader.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
class PlayerActions extends HookConsumerWidget {
|
class PlayerActions extends HookConsumerWidget {
|
||||||
final MainAxisAlignment mainAxisAlignment;
|
final MainAxisAlignment mainAxisAlignment;
|
||||||
@ -27,7 +30,25 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||||
final Playback playback = ref.watch(playbackProvider);
|
final Playback playback = ref.watch(playbackProvider);
|
||||||
final Auth auth = ref.watch(authProvider);
|
final Auth auth = ref.watch(authProvider);
|
||||||
|
final downloader = ref.watch(downloaderProvider);
|
||||||
final update = useForceUpdate();
|
final update = useForceUpdate();
|
||||||
|
final isInQueue =
|
||||||
|
downloader.inQueue.any((element) => element.id == playback.track?.id);
|
||||||
|
final localTracks = ref.watch(localTracksProvider).value;
|
||||||
|
|
||||||
|
final isDownloaded = useMemoized(() {
|
||||||
|
return localTracks?.any(
|
||||||
|
(element) =>
|
||||||
|
element.name == playback.track?.name &&
|
||||||
|
element.album?.name == playback.track?.album?.name &&
|
||||||
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
|
element.artists ?? []) ==
|
||||||
|
TypeConversionUtils.artists_X_String<Artist>(
|
||||||
|
playback.track?.artists ?? []),
|
||||||
|
) ==
|
||||||
|
true;
|
||||||
|
}, [localTracks, playback.track]);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: mainAxisAlignment,
|
mainAxisAlignment: mainAxisAlignment,
|
||||||
children: [
|
children: [
|
||||||
@ -56,9 +77,25 @@ class PlayerActions extends HookConsumerWidget {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
if (!kIsWeb)
|
if (!kIsWeb)
|
||||||
DownloadTrackButton(
|
if (isInQueue)
|
||||||
track: playback.track,
|
const SizedBox(
|
||||||
),
|
child: CircularProgressIndicator.adaptive(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isDownloaded
|
||||||
|
? Icons.download_done_rounded
|
||||||
|
: Icons.download_rounded,
|
||||||
|
),
|
||||||
|
onPressed: playback.track != null
|
||||||
|
? () => downloader.addToQueue(playback.track!)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
if (auth.isLoggedIn)
|
if (auth.isLoggedIn)
|
||||||
FutureBuilder<bool>(
|
FutureBuilder<bool>(
|
||||||
future: playback.track?.id != null
|
future: playback.track?.id != null
|
||||||
|
@ -31,25 +31,26 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<Duration>(
|
StreamBuilder<Duration>(
|
||||||
stream: playback.player.onPositionChanged,
|
stream: playback.player.onPositionChanged,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
|
||||||
duration.inMinutes.remainder(60));
|
duration.inMinutes.remainder(60));
|
||||||
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
|
||||||
duration.inSeconds.remainder(60));
|
duration.inSeconds.remainder(60));
|
||||||
final currentMinutes = snapshot.hasData
|
final currentMinutes = snapshot.hasData
|
||||||
? PrimitiveUtils.zeroPadNumStr(
|
? PrimitiveUtils.zeroPadNumStr(
|
||||||
snapshot.data!.inMinutes.remainder(60))
|
snapshot.data!.inMinutes.remainder(60))
|
||||||
: "00";
|
: "00";
|
||||||
final currentSeconds = snapshot.hasData
|
final currentSeconds = snapshot.hasData
|
||||||
? PrimitiveUtils.zeroPadNumStr(
|
? PrimitiveUtils.zeroPadNumStr(
|
||||||
snapshot.data!.inSeconds.remainder(60))
|
snapshot.data!.inSeconds.remainder(60))
|
||||||
: "00";
|
: "00";
|
||||||
|
|
||||||
final sliderMax = duration.inSeconds;
|
final sliderMax = duration.inSeconds;
|
||||||
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
final sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||||
|
|
||||||
return HookBuilder(builder: (context) {
|
return HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
final progressStatic =
|
final progressStatic =
|
||||||
(sliderMax == 0 || sliderValue > sliderMax)
|
(sliderMax == 0 || sliderValue > sliderMax)
|
||||||
? 0
|
? 0
|
||||||
@ -84,7 +85,9 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
activeColor: iconColor,
|
activeColor: iconColor,
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -97,26 +100,25 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.shuffle_rounded),
|
icon: Icon(
|
||||||
color: playback.isShuffled
|
playback.isLoop
|
||||||
? Theme.of(context).primaryColor
|
? Icons.repeat_one_rounded
|
||||||
: iconColor,
|
: playback.isShuffled
|
||||||
onPressed: () {
|
? Icons.shuffle_rounded
|
||||||
if (playback.track == null || playback.playlist == null) {
|
: Icons.repeat_rounded,
|
||||||
return;
|
),
|
||||||
}
|
onPressed: playback.track == null || playback.playlist == null
|
||||||
try {
|
? null
|
||||||
playback.toggleShuffle();
|
: playback.cyclePlaybackMode,
|
||||||
} catch (e, stack) {
|
),
|
||||||
logger.e("onShuffle", e, stack);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
icon: const Icon(Icons.skip_previous_rounded),
|
||||||
color: iconColor,
|
color: iconColor,
|
||||||
@ -125,7 +127,11 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
}),
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: playback.status == PlaybackStatus.loading
|
icon: playback.status == PlaybackStatus.loading
|
||||||
? const CircularProgressIndicator()
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
playback.isPlaying
|
playback.isPlaying
|
||||||
? Icons.pause_rounded
|
? Icons.pause_rounded
|
||||||
@ -154,6 +160,7 @@ class PlayerControls extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 5)
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:spotube/hooks/playback.dart';
|
|||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
|
|
||||||
class PlayerOverlay extends HookConsumerWidget {
|
class PlayerOverlay extends HookConsumerWidget {
|
||||||
final String albumArt;
|
final String albumArt;
|
||||||
@ -21,6 +22,9 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
final paletteColor = usePaletteColor(albumArt, ref);
|
final paletteColor = usePaletteColor(albumArt, ref);
|
||||||
|
final layoutMode = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.layoutMode),
|
||||||
|
);
|
||||||
|
|
||||||
var isHome = GoRouter.of(context).location == "/";
|
var isHome = GoRouter.of(context).location == "/";
|
||||||
final isAllowedPage = ["/playlist/", "/album/"].any(
|
final isAllowedPage = ["/playlist/", "/album/"].any(
|
||||||
@ -36,8 +40,17 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 2500),
|
duration: const Duration(milliseconds: 2500),
|
||||||
right: (breakpoint.isMd && !isAllowedPage ? 10 : 5),
|
right: (breakpoint.isMd && !isAllowedPage ? 10 : 5),
|
||||||
left: (breakpoint.isSm || isAllowedPage ? 5 : 90),
|
left: (layoutMode == LayoutMode.compact ||
|
||||||
bottom: (breakpoint.isSm && !isAllowedPage ? 63 : 10),
|
(breakpoint.isSm && layoutMode == LayoutMode.adaptive) ||
|
||||||
|
isAllowedPage
|
||||||
|
? 5
|
||||||
|
: 90),
|
||||||
|
bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) ||
|
||||||
|
(breakpoint.isSm &&
|
||||||
|
layoutMode == LayoutMode.adaptive &&
|
||||||
|
!isAllowedPage)
|
||||||
|
? 63
|
||||||
|
: 10,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onVerticalDragEnd: (details) {
|
onVerticalDragEnd: (details) {
|
||||||
int sensitivity = 8;
|
int sensitivity = 8;
|
||||||
|
@ -113,6 +113,7 @@ class PlayerQueue extends HookConsumerWidget {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
thumbnailUrl: TypeConversionUtils.image_X_UrlString(
|
thumbnailUrl: TypeConversionUtils.image_X_UrlString(
|
||||||
track.value.album?.images,
|
track.value.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
isActive: playback.track?.id == track.value.id,
|
isActive: playback.track?.id == track.value.id,
|
||||||
onTrackPlayButtonPressed: (currentTrack) async {
|
onTrackPlayButtonPressed: (currentTrack) async {
|
||||||
|
@ -42,6 +42,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
() => TypeConversionUtils.image_X_UrlString(
|
() => TypeConversionUtils.image_X_UrlString(
|
||||||
currentTrack?.album?.images,
|
currentTrack?.album?.images,
|
||||||
index: (currentTrack?.album?.images?.length ?? 1) - 1,
|
index: (currentTrack?.album?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
[currentTrack?.album?.images],
|
[currentTrack?.album?.images],
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
return PlaybuttonCard(
|
return PlaybuttonCard(
|
||||||
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
|
||||||
title: playlist.name!,
|
title: playlist.name!,
|
||||||
imageUrl: TypeConversionUtils.image_X_UrlString(playlist.images),
|
imageUrl: TypeConversionUtils.image_X_UrlString(
|
||||||
|
playlist.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
isPlaying: isPlaylistPlaying && playback.isPlaying,
|
||||||
isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying,
|
isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -56,7 +59,10 @@ class PlaylistCard extends HookConsumerWidget {
|
|||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: playlist.id!,
|
id: playlist.id!,
|
||||||
name: playlist.name!,
|
name: playlist.name!,
|
||||||
thumbnail: TypeConversionUtils.image_X_UrlString(playlist.images),
|
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||||
|
playlist.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -33,7 +33,10 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
tracks: tracks,
|
tracks: tracks,
|
||||||
id: playlist.id!,
|
id: playlist.id!,
|
||||||
name: playlist.name!,
|
name: playlist.name!,
|
||||||
thumbnail: TypeConversionUtils.image_X_UrlString(playlist.images),
|
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||||
|
playlist.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||||
);
|
);
|
||||||
@ -58,7 +61,10 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||||
|
|
||||||
final titleImage = useMemoized(
|
final titleImage = useMemoized(
|
||||||
() => TypeConversionUtils.image_X_UrlString(playlist.images),
|
() => TypeConversionUtils.image_X_UrlString(
|
||||||
|
playlist.images,
|
||||||
|
placeholder: ImagePlaceholder.collection,
|
||||||
|
),
|
||||||
[playlist.images]);
|
[playlist.images]);
|
||||||
|
|
||||||
final color = usePaletteGenerator(
|
final color = usePaletteGenerator(
|
||||||
@ -78,6 +84,11 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
onPlay: ([track]) {
|
onPlay: ([track]) {
|
||||||
if (tracksSnapshot.asData?.value != null) {
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
|
playPlaylist(
|
||||||
|
playback,
|
||||||
|
tracksSnapshot.asData!.value,
|
||||||
|
);
|
||||||
|
} else if (isPlaylistPlaying && track != null) {
|
||||||
playPlaylist(
|
playPlaylist(
|
||||||
playback,
|
playback,
|
||||||
tracksSnapshot.asData!.value,
|
tracksSnapshot.asData!.value,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
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';
|
||||||
@ -110,7 +111,9 @@ class Search extends HookConsumerWidget {
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
thumbnailUrl:
|
thumbnailUrl:
|
||||||
TypeConversionUtils.image_X_UrlString(
|
TypeConversionUtils.image_X_UrlString(
|
||||||
track.value.album?.images),
|
track.value.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
isActive: playback.track?.id == track.value.id,
|
isActive: playback.track?.id == track.value.id,
|
||||||
onTrackPlayButtonPressed: (currentTrack) async {
|
onTrackPlayButtonPressed: (currentTrack) async {
|
||||||
var isPlaylistPlaying =
|
var isPlaylistPlaying =
|
||||||
@ -126,6 +129,8 @@ class Search extends HookConsumerWidget {
|
|||||||
thumbnail: TypeConversionUtils
|
thumbnail: TypeConversionUtils
|
||||||
.image_X_UrlString(
|
.image_X_UrlString(
|
||||||
currentTrack.album?.images,
|
currentTrack.album?.images,
|
||||||
|
placeholder:
|
||||||
|
ImagePlaceholder.albumArt,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -143,19 +148,28 @@ class Search extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Scrollbar(
|
ScrollConfiguration(
|
||||||
controller: albumController,
|
behavior:
|
||||||
child: SingleChildScrollView(
|
ScrollConfiguration.of(context).copyWith(
|
||||||
scrollDirection: Axis.horizontal,
|
dragDevices: {
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: Scrollbar(
|
||||||
controller: albumController,
|
controller: albumController,
|
||||||
child: Row(
|
child: SingleChildScrollView(
|
||||||
children: albums.map((album) {
|
scrollDirection: Axis.horizontal,
|
||||||
return AlbumCard(
|
controller: albumController,
|
||||||
TypeConversionUtils.simpleAlbum_X_Album(
|
child: Row(
|
||||||
album,
|
children: albums.map((album) {
|
||||||
),
|
return AlbumCard(
|
||||||
);
|
TypeConversionUtils.simpleAlbum_X_Album(
|
||||||
}).toList(),
|
album,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -166,21 +180,30 @@ class Search extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Scrollbar(
|
ScrollConfiguration(
|
||||||
controller: artistController,
|
behavior:
|
||||||
child: SingleChildScrollView(
|
ScrollConfiguration.of(context).copyWith(
|
||||||
scrollDirection: Axis.horizontal,
|
dragDevices: {
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: Scrollbar(
|
||||||
controller: artistController,
|
controller: artistController,
|
||||||
child: Row(
|
child: SingleChildScrollView(
|
||||||
children: artists
|
scrollDirection: Axis.horizontal,
|
||||||
.map(
|
controller: artistController,
|
||||||
(artist) => Container(
|
child: Row(
|
||||||
margin: const EdgeInsets.symmetric(
|
children: artists
|
||||||
horizontal: 15),
|
.map(
|
||||||
child: ArtistCard(artist),
|
(artist) => Container(
|
||||||
),
|
margin: const EdgeInsets.symmetric(
|
||||||
)
|
horizontal: 15),
|
||||||
.toList(),
|
child: ArtistCard(artist),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -191,20 +214,30 @@ class Search extends HookConsumerWidget {
|
|||||||
style: Theme.of(context).textTheme.headline5,
|
style: Theme.of(context).textTheme.headline5,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Scrollbar(
|
ScrollConfiguration(
|
||||||
scrollbarOrientation: breakpoint > Breakpoints.md
|
behavior:
|
||||||
? ScrollbarOrientation.bottom
|
ScrollConfiguration.of(context).copyWith(
|
||||||
: ScrollbarOrientation.top,
|
dragDevices: {
|
||||||
controller: playlistController,
|
PointerDeviceKind.touch,
|
||||||
child: SingleChildScrollView(
|
PointerDeviceKind.mouse,
|
||||||
scrollDirection: Axis.horizontal,
|
},
|
||||||
|
),
|
||||||
|
child: Scrollbar(
|
||||||
|
scrollbarOrientation:
|
||||||
|
breakpoint > Breakpoints.md
|
||||||
|
? ScrollbarOrientation.bottom
|
||||||
|
: ScrollbarOrientation.top,
|
||||||
controller: playlistController,
|
controller: playlistController,
|
||||||
child: Row(
|
child: SingleChildScrollView(
|
||||||
children: playlists
|
scrollDirection: Axis.horizontal,
|
||||||
.map(
|
controller: playlistController,
|
||||||
(playlist) => PlaylistCard(playlist),
|
child: Row(
|
||||||
)
|
children: playlists
|
||||||
.toList(),
|
.map(
|
||||||
|
(playlist) => PlaylistCard(playlist),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -24,9 +24,10 @@ class About extends HookWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final info = usePackageInfo(
|
final info = usePackageInfo(
|
||||||
appName: "Spotube",
|
appName: "Spotube",
|
||||||
packageName: "oss.krtirtho.Spotube",
|
packageName: "oss.krtirtho.Spotube",
|
||||||
version: "2.3.0");
|
version: "2.4.1",
|
||||||
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(Icons.info_outline_rounded),
|
leading: Icon(Icons.info_outline_rounded),
|
||||||
|
@ -123,6 +123,40 @@ class Settings extends HookConsumerWidget {
|
|||||||
style:
|
style:
|
||||||
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
),
|
),
|
||||||
|
AdaptiveListTile(
|
||||||
|
leading: const Icon(Icons.dashboard_rounded),
|
||||||
|
title: const Text("Layout Mode"),
|
||||||
|
subtitle: const Text(
|
||||||
|
"Override responsive layout mode settings",
|
||||||
|
),
|
||||||
|
trailing: (context, update) => DropdownButton<LayoutMode>(
|
||||||
|
value: preferences.layoutMode,
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text(
|
||||||
|
"Adaptive",
|
||||||
|
),
|
||||||
|
value: LayoutMode.adaptive,
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text(
|
||||||
|
"Compact",
|
||||||
|
),
|
||||||
|
value: LayoutMode.compact,
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
child: Text("Extended"),
|
||||||
|
value: LayoutMode.extended,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
preferences.setLayoutMode(value);
|
||||||
|
update?.call(() {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
AdaptiveListTile(
|
AdaptiveListTile(
|
||||||
leading: const Icon(Icons.dark_mode_outlined),
|
leading: const Icon(Icons.dark_mode_outlined),
|
||||||
title: const Text("Theme"),
|
title: const Text("Theme"),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
|
|
||||||
class DownloadConfirmationDialog extends StatelessWidget {
|
class DownloadConfirmationDialog extends StatelessWidget {
|
||||||
const DownloadConfirmationDialog({Key? key}) : super(key: key);
|
const DownloadConfirmationDialog({Key? key}) : super(key: key);
|
||||||
@ -9,11 +9,11 @@ class DownloadConfirmationDialog extends StatelessWidget {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
contentPadding: const EdgeInsets.all(15),
|
contentPadding: const EdgeInsets.all(15),
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: const [
|
||||||
const Text("Are you sure?"),
|
Text("Are you sure?"),
|
||||||
const SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
CachedNetworkImage(
|
UniversalImage(
|
||||||
imageUrl:
|
path:
|
||||||
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
|
@ -1,228 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
|
||||||
import 'package:spotube/provider/Playback.dart';
|
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
enum TrackStatus { downloading, idle, done }
|
|
||||||
|
|
||||||
class DownloadTrackButton extends HookConsumerWidget {
|
|
||||||
final Track? track;
|
|
||||||
const DownloadTrackButton({Key? key, this.track}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, ref) {
|
|
||||||
final UserPreferences preferences = ref.watch(userPreferencesProvider);
|
|
||||||
final Playback playback = ref.watch(playbackProvider);
|
|
||||||
final status = useState<TrackStatus>(TrackStatus.idle);
|
|
||||||
YoutubeExplode yt = useMemoized(() => YoutubeExplode());
|
|
||||||
|
|
||||||
final outputFile = useState<File?>(null);
|
|
||||||
String fileName =
|
|
||||||
"${track?.name} - ${TypeConversionUtils.artists_X_String<Artist>(track?.artists ?? [])}";
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
(() async {
|
|
||||||
outputFile.value =
|
|
||||||
File(path.join(preferences.downloadLocation, "$fileName.mp3"));
|
|
||||||
}());
|
|
||||||
return null;
|
|
||||||
}, [fileName, track, preferences.downloadLocation]);
|
|
||||||
|
|
||||||
final _downloadTrack = useCallback(() async {
|
|
||||||
try {
|
|
||||||
if (track == null || outputFile.value == null) return;
|
|
||||||
if ((kIsMobile) &&
|
|
||||||
!await Permission.storage.isGranted &&
|
|
||||||
!await Permission.storage.isPermanentlyDenied) {
|
|
||||||
final status = await Permission.storage.request();
|
|
||||||
if (!status.isGranted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content:
|
|
||||||
Text("Couldn't download track. Not enough permissions"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StreamManifest manifest = await yt.videos.streamsClient
|
|
||||||
.getManifest((track as SpotubeTrack).ytTrack.url);
|
|
||||||
|
|
||||||
File outputLyricsFile = File(
|
|
||||||
path.join(preferences.downloadLocation, "$fileName-lyrics.txt"));
|
|
||||||
|
|
||||||
if (await outputFile.value!.exists()) {
|
|
||||||
final shouldReplace = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return ReplaceDownloadedFileDialog(track: track!);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (shouldReplace != true) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final audioStream = yt.videos.streamsClient
|
|
||||||
.get(
|
|
||||||
manifest.audioOnly
|
|
||||||
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
|
||||||
.withHighestBitrate(),
|
|
||||||
)
|
|
||||||
.asBroadcastStream();
|
|
||||||
|
|
||||||
final statusCb = audioStream.listen(
|
|
||||||
(event) {
|
|
||||||
if (status.value != TrackStatus.downloading) {
|
|
||||||
status.value = TrackStatus.downloading;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () async {
|
|
||||||
status.value = TrackStatus.done;
|
|
||||||
ref.refresh(localTracksProvider);
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 3),
|
|
||||||
() {
|
|
||||||
if (status.value == TrackStatus.done) {
|
|
||||||
status.value = TrackStatus.idle;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!await outputFile.value!.exists()) {
|
|
||||||
await outputFile.value!.create(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
IOSink outputFileStream = outputFile.value!.openWrite();
|
|
||||||
await audioStream.pipe(outputFileStream);
|
|
||||||
await outputFileStream.flush();
|
|
||||||
await outputFileStream.close().then((value) async {
|
|
||||||
if (status.value == TrackStatus.downloading) {
|
|
||||||
status.value = TrackStatus.done;
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 3),
|
|
||||||
() {
|
|
||||||
if (status.value == TrackStatus.done) {
|
|
||||||
status.value = TrackStatus.idle;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return statusCb.cancel();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (preferences.saveTrackLyrics && playback.track != null) {
|
|
||||||
if (!await outputLyricsFile.exists()) {
|
|
||||||
await outputLyricsFile.create(recursive: true);
|
|
||||||
}
|
|
||||||
final lyrics = await ServiceUtils.getLyrics(
|
|
||||||
playback.track!.name!,
|
|
||||||
playback.track!.artists
|
|
||||||
?.map((s) => s.name)
|
|
||||||
.whereNotNull()
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
apiKey: preferences.geniusAccessToken,
|
|
||||||
optimizeQuery: true,
|
|
||||||
);
|
|
||||||
if (lyrics != null) {
|
|
||||||
await outputLyricsFile.writeAsString(
|
|
||||||
"$lyrics\n\nPowered by genius.com",
|
|
||||||
mode: FileMode.writeOnly,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} on FileSystemException catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
content: Text("Download Failed. ${e.message} ${e.path}"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
track,
|
|
||||||
status,
|
|
||||||
yt,
|
|
||||||
preferences.saveTrackLyrics,
|
|
||||||
playback.track,
|
|
||||||
outputFile.value,
|
|
||||||
preferences.downloadLocation,
|
|
||||||
fileName
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
return () => yt.close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
final outputFileExists = useMemoized(
|
|
||||||
() => outputFile.value?.existsSync() == true,
|
|
||||||
[outputFile.value, status.value, track],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status.value == TrackStatus.downloading) {
|
|
||||||
return const SizedBox(
|
|
||||||
child: CircularProgressIndicator.adaptive(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
);
|
|
||||||
} else if (status.value == TrackStatus.done) {
|
|
||||||
return const Icon(Icons.download_done_rounded);
|
|
||||||
}
|
|
||||||
return IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
outputFileExists ? Icons.download_done_rounded : Icons.download_rounded,
|
|
||||||
),
|
|
||||||
onPressed: track != null &&
|
|
||||||
track is SpotubeTrack &&
|
|
||||||
playback.playlist?.isLocal != true
|
|
||||||
? _downloadTrack
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReplaceDownloadedFileDialog extends StatelessWidget {
|
|
||||||
final Track track;
|
|
||||||
const ReplaceDownloadedFileDialog({required this.track, Key? key})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text("Track ${track.name} Already Exists"),
|
|
||||||
content:
|
|
||||||
const Text("Do you want to replace the already downloaded track?"),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
child: const Text("No"),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: const Text("Yes"),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
import 'package:spotube/components/Shared/HoverBuilder.dart';
|
||||||
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
||||||
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
|
|
||||||
class PlaybuttonCard extends StatelessWidget {
|
class PlaybuttonCard extends StatelessWidget {
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
@ -55,8 +56,8 @@ class PlaybuttonCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: UniversalImage(
|
||||||
imageUrl: imageUrl,
|
path: imageUrl,
|
||||||
placeholder: (context, url) =>
|
placeholder: (context, url) =>
|
||||||
Image.asset("assets/placeholder.png"),
|
Image.asset("assets/placeholder.png"),
|
||||||
),
|
),
|
||||||
|
31
lib/components/Shared/ReplaceDownloadedFileDialog.dart
Normal file
31
lib/components/Shared/ReplaceDownloadedFileDialog.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
class ReplaceDownloadedFileDialog extends StatelessWidget {
|
||||||
|
final Track track;
|
||||||
|
const ReplaceDownloadedFileDialog({required this.track, Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text("Track ${track.name} Already Exists"),
|
||||||
|
content:
|
||||||
|
const Text("Do you want to replace the already downloaded track?"),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text("No"),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Yes"),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||||
|
import 'package:spotube/components/Shared/UniversalImage.dart';
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
|
import 'package:spotube/hooks/useCustomStatusBarColor.dart';
|
||||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
@ -175,9 +176,7 @@ class TrackCollectionView extends HookConsumerWidget {
|
|||||||
const BoxConstraints(maxHeight: 200),
|
const BoxConstraints(maxHeight: 200),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: CachedNetworkImage(
|
child: UniversalImage(path: titleImage),
|
||||||
imageUrl: titleImage,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
|
@ -149,6 +149,7 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
String? thumbnailUrl = TypeConversionUtils.image_X_UrlString(
|
String? thumbnailUrl = TypeConversionUtils.image_X_UrlString(
|
||||||
track.value.album?.images,
|
track.value.album?.images,
|
||||||
index: (track.value.album?.images?.length ?? 1) - 1,
|
index: (track.value.album?.images?.length ?? 1) - 1,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
);
|
);
|
||||||
String duration =
|
String duration =
|
||||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
|
@ -12,6 +12,7 @@ extension VideoFromCacheTrackExtension on Video {
|
|||||||
cacheTrack.uploadDate != null
|
cacheTrack.uploadDate != null
|
||||||
? DateTime.tryParse(cacheTrack.uploadDate!)
|
? DateTime.tryParse(cacheTrack.uploadDate!)
|
||||||
: null,
|
: null,
|
||||||
|
cacheTrack.uploadDate,
|
||||||
cacheTrack.publishDate != null
|
cacheTrack.publishDate != null
|
||||||
? DateTime.tryParse(cacheTrack.publishDate!)
|
? DateTime.tryParse(cacheTrack.publishDate!)
|
||||||
: null,
|
: null,
|
||||||
@ -69,6 +70,7 @@ extension VideoToJson on Video {
|
|||||||
map["author"],
|
map["author"],
|
||||||
ChannelId(map["channelId"]),
|
ChannelId(map["channelId"]),
|
||||||
DateTime.tryParse(map["uploadDate"]),
|
DateTime.tryParse(map["uploadDate"]),
|
||||||
|
map["uploadDate"],
|
||||||
DateTime.tryParse(map["publishDate"]),
|
DateTime.tryParse(map["publishDate"]),
|
||||||
map["description"],
|
map["description"],
|
||||||
parseDuration(map["duration"]),
|
parseDuration(map["duration"]),
|
||||||
|
@ -8,7 +8,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
import 'package:spotube/components/Shared/ReplaceDownloadedFileDialog.dart';
|
||||||
import 'package:spotube/entities/CacheTrack.dart';
|
import 'package:spotube/entities/CacheTrack.dart';
|
||||||
import 'package:spotube/models/GoRouteDeclarations.dart';
|
import 'package:spotube/models/GoRouteDeclarations.dart';
|
||||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
|
@ -1,157 +0,0 @@
|
|||||||
import 'package:dart_tags/dart_tags.dart';
|
|
||||||
|
|
||||||
class Id3Tags {
|
|
||||||
Id3Tags({
|
|
||||||
this.tsse,
|
|
||||||
this.title,
|
|
||||||
this.album,
|
|
||||||
this.tpe2,
|
|
||||||
this.comment,
|
|
||||||
this.tcop,
|
|
||||||
this.tdrc,
|
|
||||||
this.genre,
|
|
||||||
this.picture,
|
|
||||||
});
|
|
||||||
|
|
||||||
String? tsse;
|
|
||||||
String? title;
|
|
||||||
String? album;
|
|
||||||
String? tpe2;
|
|
||||||
Comment? comment;
|
|
||||||
String? tcop;
|
|
||||||
String? tdrc;
|
|
||||||
String? genre;
|
|
||||||
AttachedPicture? picture;
|
|
||||||
|
|
||||||
factory Id3Tags.fromJson(Map<String, dynamic> json) => Id3Tags(
|
|
||||||
tsse: json["TSSE"],
|
|
||||||
title: json["title"],
|
|
||||||
album: json["album"],
|
|
||||||
tpe2: json["TPE2"],
|
|
||||||
comment: json["comment"]?["eng:"] is Comment
|
|
||||||
? json["comment"]["eng:"]
|
|
||||||
: CommentJson.fromJson(Map.from(
|
|
||||||
json["comment"]?["eng:"] ?? {},
|
|
||||||
)),
|
|
||||||
tcop: json["TCOP"],
|
|
||||||
tdrc: json["TDRC"],
|
|
||||||
genre: json["genre"],
|
|
||||||
picture: json["picture"]?["Cover (front)"] is AttachedPicture
|
|
||||||
? json["picture"]["Cover (front)"]
|
|
||||||
: AttachedPictureJson.fromJson(Map.from(
|
|
||||||
json["picture"]?["Cover (front)"] ?? {},
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
|
|
||||||
factory Id3Tags.fromId3v1Tags(Id3v1Tags v1tags) => Id3Tags(
|
|
||||||
album: v1tags.album,
|
|
||||||
comment: Comment("", "", v1tags.comment ?? ""),
|
|
||||||
genre: v1tags.genre,
|
|
||||||
title: v1tags.title,
|
|
||||||
tcop: v1tags.year,
|
|
||||||
tdrc: v1tags.year,
|
|
||||||
tpe2: v1tags.artist,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
"TSSE": tsse,
|
|
||||||
"title": title,
|
|
||||||
"album": album,
|
|
||||||
"TPE2": tpe2,
|
|
||||||
"comment": comment,
|
|
||||||
"TCOP": tcop,
|
|
||||||
"TDRC": tdrc,
|
|
||||||
"genre": genre,
|
|
||||||
"picture": picture,
|
|
||||||
};
|
|
||||||
|
|
||||||
String? get artist => tpe2;
|
|
||||||
String? get year => tdrc;
|
|
||||||
|
|
||||||
Map<String, String> toAndroidJson(String artwork) {
|
|
||||||
return {
|
|
||||||
"title": title ?? "Unknown",
|
|
||||||
"artist": artist ?? "Unknown",
|
|
||||||
"album": album ?? "Unknown",
|
|
||||||
"genre": genre ?? "Unknown",
|
|
||||||
"artwork": artwork,
|
|
||||||
"year": year ?? "Unknown",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CommentJson on Comment {
|
|
||||||
static fromJson(Map<String, dynamic> json) => Comment(
|
|
||||||
json["lang"] ?? "",
|
|
||||||
json["description"] ?? "",
|
|
||||||
json["comment"] ?? "",
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
"comment": comment,
|
|
||||||
"description": description,
|
|
||||||
"key": key,
|
|
||||||
"lang": lang,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachedPictureJson on AttachedPicture {
|
|
||||||
static fromJson(Map<String, dynamic> json) => AttachedPicture(
|
|
||||||
json["mime"] ?? "",
|
|
||||||
json["imageTypeCode"] ?? 0,
|
|
||||||
json["description"] ?? "",
|
|
||||||
List<int>.from(json["imageData"] ?? []),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
"description": description,
|
|
||||||
"imageData": imageData,
|
|
||||||
"imageData64": imageData64,
|
|
||||||
"imageType": imageType,
|
|
||||||
"imageTypeCode": imageTypeCode,
|
|
||||||
"key": key,
|
|
||||||
"mime": mime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class Id3v1Tags {
|
|
||||||
String? title;
|
|
||||||
String? artist;
|
|
||||||
String? album;
|
|
||||||
String? year;
|
|
||||||
String? comment;
|
|
||||||
String? track;
|
|
||||||
String? genre;
|
|
||||||
|
|
||||||
Id3v1Tags({
|
|
||||||
this.title,
|
|
||||||
this.artist,
|
|
||||||
this.album,
|
|
||||||
this.year,
|
|
||||||
this.comment,
|
|
||||||
this.track,
|
|
||||||
this.genre,
|
|
||||||
});
|
|
||||||
|
|
||||||
Id3v1Tags.fromJson(Map<String, dynamic> json) {
|
|
||||||
title = json['title'];
|
|
||||||
artist = json['artist'];
|
|
||||||
album = json['album'];
|
|
||||||
year = json['year'];
|
|
||||||
comment = json['comment'];
|
|
||||||
track = json['track'];
|
|
||||||
genre = json['genre'];
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'title': title,
|
|
||||||
'artist': artist,
|
|
||||||
'album': album,
|
|
||||||
'year': year,
|
|
||||||
'comment': comment,
|
|
||||||
'track': track,
|
|
||||||
'genre': genre,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
final _loggerFactory = _SpotubeLogger();
|
final _loggerFactory = _SpotubeLogger();
|
||||||
|
|
||||||
@ -18,8 +19,11 @@ class _SpotubeLogger extends Logger {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void log(Level level, message, [error, StackTrace? stackTrace]) {
|
void log(Level level, message, [error, StackTrace? stackTrace]) {
|
||||||
getApplicationDocumentsDirectory().then((dir) async {
|
(kIsAndroid
|
||||||
final file = File(path.join(dir.path, ".spotube_logs"));
|
? getExternalStorageDirectory()
|
||||||
|
: getApplicationDocumentsDirectory())
|
||||||
|
.then((dir) async {
|
||||||
|
final file = File(path.join(dir!.path, ".spotube_logs"));
|
||||||
if (level == Level.error) {
|
if (level == Level.error) {
|
||||||
await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace",
|
await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace",
|
||||||
mode: FileMode.writeOnlyAppend);
|
mode: FileMode.writeOnlyAppend);
|
||||||
|
@ -1,22 +1,18 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dart_tags/dart_tags.dart';
|
import 'package:flutter/widgets.dart' hide Image;
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:queue/queue.dart';
|
import 'package:queue/queue.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
|
||||||
import 'package:spotube/models/Id3Tags.dart';
|
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
|
||||||
|
|
||||||
@ -63,7 +59,7 @@ class Downloader with ChangeNotifier {
|
|||||||
RegExp(r'[/\\?%*:|"<>]'),
|
RegExp(r'[/\\?%*:|"<>]'),
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
final filename = '$cleanTitle.mp3';
|
final filename = '$cleanTitle.m4a';
|
||||||
final file = File(path.join(downloadPath, filename));
|
final file = File(path.join(downloadPath, filename));
|
||||||
try {
|
try {
|
||||||
logger.v("[addToQueue] Download starting for ${file.path}");
|
logger.v("[addToQueue] Download starting for ${file.path}");
|
||||||
@ -97,54 +93,38 @@ class Downloader with ChangeNotifier {
|
|||||||
"[addToQueue] Download of ${file.path} is done successfully",
|
"[addToQueue] Download of ${file.path} is done successfully",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tagging isn't supported in Android currently
|
logger.v(
|
||||||
if (kIsAndroid) return;
|
"[addToQueue] Writing metadata to ${file.path}",
|
||||||
|
);
|
||||||
final imageUri = TypeConversionUtils.image_X_UrlString(
|
final imageUri = TypeConversionUtils.image_X_UrlString(
|
||||||
track.album?.images ?? [],
|
track.album?.images ?? [],
|
||||||
|
placeholder: ImagePlaceholder.online,
|
||||||
);
|
);
|
||||||
final response = await get(
|
final response = await get(Uri.parse(imageUri));
|
||||||
Uri.parse(
|
|
||||||
imageUri,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final picture = AttachedPicture.base64(
|
|
||||||
response.headers["Content-Type"] ?? "image/jpeg",
|
|
||||||
3,
|
|
||||||
track.name!,
|
|
||||||
base64Encode(response.bodyBytes),
|
|
||||||
);
|
|
||||||
// write id3 metadata
|
|
||||||
final tag = Id3Tags(
|
|
||||||
album: track.album?.name,
|
|
||||||
picture: picture,
|
|
||||||
title: track.name,
|
|
||||||
genre: "Spotube",
|
|
||||||
tcop: track.ytTrack.uploadDate?.year.toString(),
|
|
||||||
tdrc: track.ytTrack.uploadDate?.year.toString(),
|
|
||||||
tpe2: TypeConversionUtils.artists_X_String<Artist>(
|
|
||||||
track.artists ?? [],
|
|
||||||
),
|
|
||||||
tsse: "",
|
|
||||||
comment: Comment(
|
|
||||||
"eng",
|
|
||||||
track.ytTrack.description,
|
|
||||||
track.ytTrack.title,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.v("[addToQueue] Writing metadata to ${file.path}");
|
await MetadataGod.writeMetadata(
|
||||||
|
file,
|
||||||
final taggedMp3 = await tagProcessor.putTagsToByteArray(
|
Metadata(
|
||||||
file.readAsBytes(),
|
title: track.name,
|
||||||
[
|
artist: track.artists?.map((a) => a.name).join(", "),
|
||||||
Tag()
|
album: track.album?.name,
|
||||||
..type = "ID3"
|
albumArtist: track.artists?.map((a) => a.name).join(", "),
|
||||||
..version = "2.4.0"
|
year: track.album?.releaseDate != null
|
||||||
..tags = tag.toJson()
|
? int.tryParse(track.album!.releaseDate!)
|
||||||
],
|
: null,
|
||||||
|
trackNumber: track.trackNumber,
|
||||||
|
discNumber: track.discNumber,
|
||||||
|
durationMs: track.durationMs?.toDouble(),
|
||||||
|
fileSize: file.lengthSync(),
|
||||||
|
trackTotal: track.album?.tracks?.length,
|
||||||
|
picture: response.headers['content-type'] != null
|
||||||
|
? Image(
|
||||||
|
data: response.bodyBytes,
|
||||||
|
mimeType: response.headers['content-type']!,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await file.writeAsBytes(taggedMp3);
|
|
||||||
logger.v(
|
logger.v(
|
||||||
"[addToQueue] Writing metadata to ${file.path} is successful",
|
"[addToQueue] Writing metadata to ${file.path} is successful",
|
||||||
);
|
);
|
||||||
|
@ -37,9 +37,15 @@ enum AudioQuality {
|
|||||||
low,
|
low,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlaybackMode {
|
||||||
|
repeat,
|
||||||
|
shuffle,
|
||||||
|
normal,
|
||||||
|
}
|
||||||
|
|
||||||
class Playback extends PersistedChangeNotifier {
|
class Playback extends PersistedChangeNotifier {
|
||||||
// player properties
|
// player properties
|
||||||
bool isShuffled;
|
PlaybackMode playbackMode;
|
||||||
bool isPlaying;
|
bool isPlaying;
|
||||||
Duration currentDuration;
|
Duration currentDuration;
|
||||||
double volume;
|
double volume;
|
||||||
@ -72,9 +78,9 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
required this.youtube,
|
required this.youtube,
|
||||||
required this.ref,
|
required this.ref,
|
||||||
this.mobileAudioService,
|
this.mobileAudioService,
|
||||||
}) : volume = 0,
|
}) : volume = 1,
|
||||||
isShuffled = false,
|
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
|
playbackMode = PlaybackMode.normal,
|
||||||
currentDuration = Duration.zero,
|
currentDuration = Duration.zero,
|
||||||
_subscriptions = [],
|
_subscriptions = [],
|
||||||
status = PlaybackStatus.idle,
|
status = PlaybackStatus.idle,
|
||||||
@ -93,6 +99,10 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
await player.setVolume(volume);
|
await player.setVolume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addListener(() {
|
||||||
|
_linuxAudioService?.player.updateProperties(this);
|
||||||
|
});
|
||||||
|
|
||||||
_subscriptions.addAll([
|
_subscriptions.addAll([
|
||||||
player.onPlayerStateChanged.listen(
|
player.onPlayerStateChanged.listen(
|
||||||
(state) async {
|
(state) async {
|
||||||
@ -102,7 +112,13 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
),
|
),
|
||||||
player.onPlayerComplete.listen((_) {
|
player.onPlayerComplete.listen((_) {
|
||||||
if (track?.id != null) {
|
if (track?.id != null) {
|
||||||
seekForward();
|
if (isLoop) {
|
||||||
|
final prevTrack = track;
|
||||||
|
track = null;
|
||||||
|
play(prevTrack!);
|
||||||
|
} else if (playlist != null) {
|
||||||
|
seekForward();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
status = PlaybackStatus.idle;
|
status = PlaybackStatus.idle;
|
||||||
@ -210,6 +226,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
artUri: Uri.parse(
|
artUri: Uri.parse(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
TypeConversionUtils.image_X_UrlString(
|
||||||
track.album?.images,
|
track.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.online,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
duration: track.ytTrack.duration,
|
duration: track.ytTrack.duration,
|
||||||
@ -249,12 +266,26 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
isPlaying ? await pause() : await resume();
|
isPlaying ? await pause() : await resume();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleShuffle() {
|
void cyclePlaybackMode() {
|
||||||
final result = isShuffled ? playlist?.unshuffle() : playlist?.shuffle();
|
switch (playbackMode) {
|
||||||
if (result == true) {
|
case PlaybackMode.normal:
|
||||||
isShuffled = !isShuffled;
|
playbackMode = PlaybackMode.shuffle;
|
||||||
notifyListeners();
|
playlist?.shuffle();
|
||||||
|
break;
|
||||||
|
case PlaybackMode.shuffle:
|
||||||
|
playbackMode = PlaybackMode.repeat;
|
||||||
|
playlist?.unshuffle();
|
||||||
|
break;
|
||||||
|
case PlaybackMode.repeat:
|
||||||
|
playbackMode = PlaybackMode.normal;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPlaybackMode(PlaybackMode mode) {
|
||||||
|
playbackMode = mode;
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> seekPosition(Duration position) {
|
Future<void> seekPosition(Duration position) {
|
||||||
@ -272,7 +303,7 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
await player.stop();
|
await player.stop();
|
||||||
await player.release();
|
await player.release();
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
isShuffled = false;
|
playbackMode = PlaybackMode.normal;
|
||||||
playlist = null;
|
playlist = null;
|
||||||
track = null;
|
track = null;
|
||||||
status = PlaybackStatus.idle;
|
status = PlaybackStatus.idle;
|
||||||
@ -558,6 +589,10 @@ class Playback extends PersistedChangeNotifier {
|
|||||||
"volume": volume,
|
"volume": volume,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isLoop => playbackMode == PlaybackMode.repeat;
|
||||||
|
bool get isShuffled => playbackMode == PlaybackMode.shuffle;
|
||||||
|
bool get isNormal => playbackMode == PlaybackMode.normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
||||||
|
@ -133,7 +133,10 @@ final currentUserQuery = FutureProvider<User>(
|
|||||||
Image()
|
Image()
|
||||||
..height = 50
|
..height = 50
|
||||||
..width = 50
|
..width = 50
|
||||||
..url = TypeConversionUtils.image_X_UrlString(me.images),
|
..url = TypeConversionUtils.image_X_UrlString(
|
||||||
|
me.images,
|
||||||
|
placeholder: ImagePlaceholder.artist,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return me;
|
return me;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@ -14,6 +13,12 @@ import 'package:spotube/utils/platform.dart';
|
|||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
enum LayoutMode {
|
||||||
|
compact,
|
||||||
|
extended,
|
||||||
|
adaptive,
|
||||||
|
}
|
||||||
|
|
||||||
class UserPreferences extends PersistedChangeNotifier {
|
class UserPreferences extends PersistedChangeNotifier {
|
||||||
ThemeMode themeMode;
|
ThemeMode themeMode;
|
||||||
String ytSearchFormat;
|
String ytSearchFormat;
|
||||||
@ -30,11 +35,14 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
|
|
||||||
String downloadLocation;
|
String downloadLocation;
|
||||||
|
|
||||||
|
LayoutMode layoutMode;
|
||||||
|
|
||||||
UserPreferences({
|
UserPreferences({
|
||||||
required this.geniusAccessToken,
|
required this.geniusAccessToken,
|
||||||
required this.recommendationMarket,
|
required this.recommendationMarket,
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
required this.ytSearchFormat,
|
required this.ytSearchFormat,
|
||||||
|
required this.layoutMode,
|
||||||
this.saveTrackLyrics = false,
|
this.saveTrackLyrics = false,
|
||||||
this.accentColorScheme = Colors.green,
|
this.accentColorScheme = Colors.green,
|
||||||
this.backgroundColorScheme = Colors.grey,
|
this.backgroundColorScheme = Colors.grey,
|
||||||
@ -126,6 +134,12 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
updatePersistence();
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setLayoutMode(LayoutMode mode) {
|
||||||
|
layoutMode = mode;
|
||||||
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> _getDefaultDownloadDirectory() async {
|
Future<String> _getDefaultDownloadDirectory() async {
|
||||||
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
||||||
return getDownloadsDirectory().then((dir) {
|
return getDownloadsDirectory().then((dir) {
|
||||||
@ -158,6 +172,11 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
|
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
|
||||||
downloadLocation =
|
downloadLocation =
|
||||||
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
|
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
|
||||||
|
|
||||||
|
layoutMode = LayoutMode.values.firstWhere(
|
||||||
|
(mode) => mode.name == map["layoutMode"],
|
||||||
|
orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -175,6 +194,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
"audioQuality": audioQuality.index,
|
"audioQuality": audioQuality.index,
|
||||||
"skipSponsorSegments": skipSponsorSegments,
|
"skipSponsorSegments": skipSponsorSegments,
|
||||||
"downloadLocation": downloadLocation,
|
"downloadLocation": downloadLocation,
|
||||||
|
"layoutMode": layoutMode.name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,5 +205,6 @@ final userPreferencesProvider = ChangeNotifierProvider(
|
|||||||
recommendationMarket: 'US',
|
recommendationMarket: 'US',
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS",
|
ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS",
|
||||||
|
layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -217,7 +217,7 @@ class _MprisMediaPlayer2 extends DBusObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MprisMediaPlayer2Player extends DBusObject {
|
class _MprisMediaPlayer2Player extends DBusObject {
|
||||||
final Playback playback;
|
Playback playback;
|
||||||
|
|
||||||
/// Creates a new object to expose on [path].
|
/// Creates a new object to expose on [path].
|
||||||
_MprisMediaPlayer2Player({
|
_MprisMediaPlayer2Player({
|
||||||
@ -275,7 +275,9 @@ class _MprisMediaPlayer2Player extends DBusObject {
|
|||||||
|
|
||||||
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
|
||||||
Future<DBusMethodResponse> setShuffle(bool value) async {
|
Future<DBusMethodResponse> setShuffle(bool value) async {
|
||||||
playback.toggleShuffle();
|
playback.setPlaybackMode(
|
||||||
|
value ? PlaybackMode.shuffle : PlaybackMode.normal,
|
||||||
|
);
|
||||||
return DBusMethodSuccessResponse();
|
return DBusMethodSuccessResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +300,9 @@ class _MprisMediaPlayer2Player extends DBusObject {
|
|||||||
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
|
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
|
||||||
"mpris:artUrl": DBusString(
|
"mpris:artUrl": DBusString(
|
||||||
TypeConversionUtils.image_X_UrlString(
|
TypeConversionUtils.image_X_UrlString(
|
||||||
playback.track?.album?.images),
|
playback.track?.album?.images,
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
"xesam:album": DBusString(playback.track!.album!.name!),
|
"xesam:album": DBusString(playback.track!.album!.name!),
|
||||||
"xesam:artist": DBusArray.string(
|
"xesam:artist": DBusArray.string(
|
||||||
@ -443,6 +447,30 @@ class _MprisMediaPlayer2Player extends DBusObject {
|
|||||||
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]);
|
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateProperties(Playback playback) async {
|
||||||
|
this.playback = playback;
|
||||||
|
return emitPropertiesChanged(
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
changedProperties: {
|
||||||
|
"PlaybackStatus": (await getPlaybackStatus()).returnValues.first,
|
||||||
|
"LoopStatus": (await getLoopStatus()).returnValues.first,
|
||||||
|
"Rate": (await getRate()).returnValues.first,
|
||||||
|
"Shuffle": (await getShuffle()).returnValues.first,
|
||||||
|
"Metadata": (await getMetadata()).returnValues.first,
|
||||||
|
"Volume": (await getVolume()).returnValues.first,
|
||||||
|
"Position": (await getPosition()).returnValues.first,
|
||||||
|
"MinimumRate": (await getMinimumRate()).returnValues.first,
|
||||||
|
"MaximumRate": (await getMaximumRate()).returnValues.first,
|
||||||
|
"CanGoNext": (await getCanGoNext()).returnValues.first,
|
||||||
|
"CanGoPrevious": (await getCanGoPrevious()).returnValues.first,
|
||||||
|
"CanPlay": (await getCanPlay()).returnValues.first,
|
||||||
|
"CanPause": (await getCanPause()).returnValues.first,
|
||||||
|
"CanSeek": (await getCanSeek()).returnValues.first,
|
||||||
|
"CanControl": (await getCanControl()).returnValues.first,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<DBusIntrospectInterface> introspect() {
|
List<DBusIntrospectInterface> introspect() {
|
||||||
return [
|
return [
|
||||||
|
@ -184,8 +184,12 @@ abstract class ServiceUtils {
|
|||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
|
|
||||||
HttpServer server =
|
HttpServer server = await HttpServer.bind(
|
||||||
await HttpServer.bind(InternetAddress.loopbackIPv4, 4304);
|
InternetAddress.loopbackIPv4,
|
||||||
|
4304,
|
||||||
|
shared: true,
|
||||||
|
);
|
||||||
|
|
||||||
logger.i("[connectIpc] Server started");
|
logger.i("[connectIpc] Server started");
|
||||||
|
|
||||||
await for (HttpRequest request in server) {
|
await for (HttpRequest request in server) {
|
||||||
|
@ -2,22 +2,38 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dart_tags/dart_tags.dart';
|
|
||||||
import 'package:flutter/widgets.dart' hide Image;
|
import 'package:flutter/widgets.dart' hide Image;
|
||||||
|
import 'package:metadata_god/metadata_god.dart' hide Image;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotube/components/Shared/LinkText.dart';
|
import 'package:spotube/components/Shared/LinkText.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/Id3Tags.dart';
|
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
|
enum ImagePlaceholder {
|
||||||
|
albumArt,
|
||||||
|
artist,
|
||||||
|
collection,
|
||||||
|
online,
|
||||||
|
}
|
||||||
|
|
||||||
abstract class TypeConversionUtils {
|
abstract class TypeConversionUtils {
|
||||||
static String image_X_UrlString(List<Image>? images, {int index = 0}) {
|
static String image_X_UrlString(
|
||||||
|
List<Image>? images, {
|
||||||
|
int index = 0,
|
||||||
|
required ImagePlaceholder placeholder,
|
||||||
|
}) {
|
||||||
|
final String placeholderUrl = {
|
||||||
|
ImagePlaceholder.albumArt: "assets/album-placeholder.png",
|
||||||
|
ImagePlaceholder.artist: "assets/user-placeholder.png",
|
||||||
|
ImagePlaceholder.collection: "assets/placeholder.png",
|
||||||
|
ImagePlaceholder.online:
|
||||||
|
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
||||||
|
}[placeholder]!;
|
||||||
return images != null && images.isNotEmpty
|
return images != null && images.isNotEmpty
|
||||||
? images[0].url!
|
? images[0].url!
|
||||||
: "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png";
|
: placeholderUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String artists_X_String<T extends ArtistSimple>(List<T> artists) {
|
static String artists_X_String<T extends ArtistSimple>(List<T> artists) {
|
||||||
@ -91,31 +107,24 @@ abstract class TypeConversionUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static SpotubeTrack localTrack_X_Track(
|
static SpotubeTrack localTrack_X_Track(
|
||||||
List<Tag> metadatas,
|
File file, {
|
||||||
File file,
|
Metadata? metadata,
|
||||||
Duration duration,
|
|
||||||
String? art,
|
String? art,
|
||||||
) {
|
}) {
|
||||||
final v2Tags =
|
|
||||||
metadatas.firstWhereOrNull((s) => s.version == "2.4.0")?.tags;
|
|
||||||
final v1Tags =
|
|
||||||
metadatas.firstWhereOrNull((s) => s.version != "2.4.0")?.tags;
|
|
||||||
final metadata = v2Tags != null
|
|
||||||
? Id3Tags.fromJson(v2Tags)
|
|
||||||
: Id3Tags.fromId3v1Tags(Id3v1Tags.fromJson(v1Tags ?? {}));
|
|
||||||
final track = SpotubeTrack(
|
final track = SpotubeTrack(
|
||||||
Video(
|
Video(
|
||||||
VideoId("dQw4w9WgXcQ"),
|
VideoId("dQw4w9WgXcQ"),
|
||||||
basenameWithoutExtension(file.path),
|
basenameWithoutExtension(file.path),
|
||||||
metadata.tpe2 ?? "",
|
metadata?.artist ?? "",
|
||||||
ChannelId(
|
ChannelId(
|
||||||
"https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
|
"https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
|
||||||
),
|
),
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
|
"",
|
||||||
DateTime.now(),
|
DateTime.now(),
|
||||||
"",
|
"",
|
||||||
duration,
|
Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
|
||||||
ThumbnailSet(metadata.title ?? ""),
|
ThumbnailSet(metadata?.title ?? ""),
|
||||||
[],
|
[],
|
||||||
const Engagement(0, 0, 0),
|
const Engagement(0, 0, 0),
|
||||||
false,
|
false,
|
||||||
@ -124,27 +133,28 @@ abstract class TypeConversionUtils {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
track.album = Album()
|
track.album = Album()
|
||||||
..name = metadata.album ?? "Spotube"
|
..name = metadata?.album ?? "Spotube"
|
||||||
..images = [if (art != null) Image()..url = art]
|
..images = [if (art != null) Image()..url = art]
|
||||||
..genres = [if (metadata.genre != null) metadata.genre!]
|
..genres = [if (metadata?.genre != null) metadata!.genre!]
|
||||||
..artists = [
|
..artists = [
|
||||||
Artist()
|
Artist()
|
||||||
..name = metadata.tpe2 ?? "Spotube"
|
..name = metadata?.albumArtist ?? "Spotube"
|
||||||
..id = metadata.tpe2 ?? "Spotube"
|
..id = metadata?.albumArtist ?? "Spotube"
|
||||||
..type = "artist",
|
..type = "artist",
|
||||||
]
|
]
|
||||||
..id = metadata.album;
|
..id = metadata?.album
|
||||||
|
..releaseDate = metadata?.year?.toString();
|
||||||
track.artists = [
|
track.artists = [
|
||||||
Artist()
|
Artist()
|
||||||
..name = metadata.tpe2 ?? "Spotube"
|
..name = metadata?.artist ?? "Spotube"
|
||||||
..id = metadata.tpe2 ?? "Spotube"
|
..id = metadata?.artist ?? "Spotube"
|
||||||
];
|
];
|
||||||
|
|
||||||
track.id = metadata.title ?? basenameWithoutExtension(file.path);
|
track.id = metadata?.title ?? basenameWithoutExtension(file.path);
|
||||||
track.name = metadata.title ?? basenameWithoutExtension(file.path);
|
track.name = metadata?.title ?? basenameWithoutExtension(file.path);
|
||||||
track.type = "track";
|
track.type = "track";
|
||||||
track.uri = file.path;
|
track.uri = file.path;
|
||||||
track.durationMs = duration.inMilliseconds;
|
track.durationMs = metadata?.durationMs?.toInt();
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||||
|
#include <metadata_god/metadata_god_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
|
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
|
||||||
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
|
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) metadata_god_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "MetadataGodPlugin");
|
||||||
|
metadata_god_plugin_register_with_registrar(metadata_god_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
audioplayers_linux
|
audioplayers_linux
|
||||||
bitsdojo_window_linux
|
bitsdojo_window_linux
|
||||||
|
metadata_god
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
197
pubspec.lock
197
pubspec.lock
@ -15,13 +15,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.0"
|
||||||
ansicolor:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ansicolor
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.1"
|
|
||||||
app_package_maker:
|
app_package_maker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -78,48 +71,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.9"
|
version: "0.0.9"
|
||||||
app_package_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_parser
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.7"
|
|
||||||
app_package_parser_apk:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_parser_apk
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.7"
|
|
||||||
app_package_parser_ipa:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_parser_ipa
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.7"
|
|
||||||
app_package_publisher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_publisher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.9"
|
|
||||||
app_package_publisher_fir:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_publisher_fir
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.9"
|
|
||||||
app_package_publisher_pgyer:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_package_publisher_pgyer
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.9"
|
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -172,9 +123,11 @@ packages:
|
|||||||
audioplayers:
|
audioplayers:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: audioplayers
|
path: "packages/audioplayers"
|
||||||
url: "https://pub.dartlang.org"
|
ref: "3ee12cd0361c0fc2f3d0303c504732d12fa8e49a"
|
||||||
source: hosted
|
resolved-ref: "3ee12cd0361c0fc2f3d0303c504732d12fa8e49a"
|
||||||
|
url: "https://github.com/bluefireteam/audioplayers.git"
|
||||||
|
source: git
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
audioplayers_android:
|
audioplayers_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@ -238,21 +191,21 @@ packages:
|
|||||||
name: bitsdojo_window
|
name: bitsdojo_window
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.5"
|
||||||
bitsdojo_window_linux:
|
bitsdojo_window_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: bitsdojo_window_linux
|
name: bitsdojo_window_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.3"
|
||||||
bitsdojo_window_macos:
|
bitsdojo_window_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: bitsdojo_window_macos
|
name: bitsdojo_window_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.3"
|
||||||
bitsdojo_window_platform_interface:
|
bitsdojo_window_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -266,7 +219,7 @@ packages:
|
|||||||
name: bitsdojo_window_windows
|
name: bitsdojo_window_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.5"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -386,20 +339,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
colorize:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: colorize
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.0"
|
|
||||||
console_bars:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: console_bars
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.5"
|
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -435,27 +374,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.3"
|
version: "2.2.3"
|
||||||
dart_tags:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: dart_tags
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.4.0"
|
|
||||||
dbus:
|
dbus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dbus
|
name: dbus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.8"
|
||||||
dio:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: dio
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "4.0.6"
|
|
||||||
dots_indicator:
|
dots_indicator:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -483,7 +408,7 @@ packages:
|
|||||||
name: ffi
|
name: ffi
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "2.0.1"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -494,10 +419,12 @@ packages:
|
|||||||
file_picker:
|
file_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: HEAD
|
||||||
source: hosted
|
resolved-ref: f9133f6d5dbf33191fc9b58655aebfd15445045a
|
||||||
version: "4.6.1"
|
url: "https://github.com/KRTirtho/flutter_file_picker.git"
|
||||||
|
source: git
|
||||||
|
version: "5.0.1"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -524,13 +451,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.9"
|
version: "0.0.9"
|
||||||
flutter_app_publisher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_app_publisher
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.9"
|
|
||||||
flutter_blurhash:
|
flutter_blurhash:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -551,7 +471,7 @@ packages:
|
|||||||
name: flutter_distributor
|
name: flutter_distributor
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.9"
|
version: "0.0.2"
|
||||||
flutter_hooks:
|
flutter_hooks:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -587,6 +507,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
flutter_rust_bridge:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_rust_bridge
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.42.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -603,7 +530,7 @@ packages:
|
|||||||
name: freezed_annotation
|
name: freezed_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "2.1.0"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -695,20 +622,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
version: "4.0.1"
|
||||||
id3:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: id3
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.2.0"
|
||||||
infinite_scroll_pagination:
|
infinite_scroll_pagination:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -793,6 +713,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.0"
|
||||||
|
metadata_god:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: metadata_god
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -800,20 +727,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.2"
|
||||||
mp3_info:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: mp3_info
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0"
|
|
||||||
msix:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: msix
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.8.18"
|
|
||||||
oauth2:
|
oauth2:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -841,7 +754,7 @@ packages:
|
|||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.2"
|
version: "1.4.3+1"
|
||||||
package_info_plus_linux:
|
package_info_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -876,7 +789,7 @@ packages:
|
|||||||
name: package_info_plus_windows
|
name: package_info_plus_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "2.0.0"
|
||||||
palette_generator:
|
palette_generator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -884,13 +797,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
version: "0.3.3"
|
||||||
parse_app_package:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: parse_app_package
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.7"
|
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -904,7 +810,7 @@ packages:
|
|||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.10"
|
version: "2.0.11"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -925,7 +831,7 @@ packages:
|
|||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.6"
|
version: "2.1.7"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -946,7 +852,7 @@ packages:
|
|||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.1.3"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1186,12 +1092,10 @@ packages:
|
|||||||
spotify:
|
spotify:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
name: spotify
|
||||||
ref: HEAD
|
url: "https://pub.dartlang.org"
|
||||||
resolved-ref: ea313e2d21c38157cd8255d248bcd7897bf51360
|
source: hosted
|
||||||
url: "https://github.com/KRTirtho/spotify-dart.git"
|
version: "0.8.0"
|
||||||
source: git
|
|
||||||
version: "0.7.0"
|
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1332,13 +1236,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
utf_convert:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: utf_convert
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.10.0+1"
|
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1375,12 +1272,12 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "3.0.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1394,7 +1291,7 @@ packages:
|
|||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.1"
|
version: "6.1.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1408,7 +1305,7 @@ packages:
|
|||||||
name: youtube_explode_dart
|
name: youtube_explode_dart
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.12.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.1 <3.0.0"
|
dart: ">=2.17.1 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
94
pubspec.yaml
94
pubspec.yaml
@ -1,47 +1,25 @@
|
|||||||
name: spotube
|
name: spotube
|
||||||
description: A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed
|
description: A lightweight free Spotify crossplatform-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed
|
||||||
|
|
||||||
# The following line prevents the package from being accidentally published to
|
publish_to: "none"
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
|
||||||
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
version: 2.4.1+14
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
|
||||||
# followed by an optional build number separated by a +.
|
|
||||||
# Both the version and the builder number may be overridden in flutter
|
|
||||||
# build by specifying --build-name and --build-number, respectively.
|
|
||||||
# In Android, build-name is used as versionName while build-number used as versionCode.
|
|
||||||
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
|
||||||
# Read more about iOS versioning at
|
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
||||||
version: 2.3.0+12
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
|
||||||
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
|
||||||
# dependencies can be manually updated by changing the version numbers below to
|
|
||||||
# the latest version available on pub.dev. To see which dependencies have newer
|
|
||||||
# versions available, run `flutter pub outdated`.
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
cached_network_image: ^3.2.0
|
cached_network_image: ^3.2.0
|
||||||
html: ^0.15.0
|
html: ^0.15.0
|
||||||
http: ^0.13.4
|
http: ^0.13.4
|
||||||
shared_preferences: ^2.0.11
|
shared_preferences: ^2.0.11
|
||||||
spotify:
|
spotify: ^0.8.0
|
||||||
git: https://github.com/KRTirtho/spotify-dart.git
|
|
||||||
url_launcher: ^6.0.17
|
url_launcher: ^6.0.17
|
||||||
youtube_explode_dart: ^1.10.8
|
youtube_explode_dart: ^1.10.8
|
||||||
bitsdojo_window: ^0.1.2
|
bitsdojo_window: ^0.1.5
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.8
|
path_provider: ^2.0.8
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
@ -54,7 +32,7 @@ dependencies:
|
|||||||
permission_handler: ^9.2.0
|
permission_handler: ^9.2.0
|
||||||
marquee: ^2.2.1
|
marquee: ^2.2.1
|
||||||
scroll_to_index: ^2.1.1
|
scroll_to_index: ^2.1.1
|
||||||
package_info_plus: ^1.4.2
|
package_info_plus: ^1.4.3
|
||||||
version: ^2.0.0
|
version: ^2.0.0
|
||||||
audio_service: ^0.18.4
|
audio_service: ^0.18.4
|
||||||
hookified_infinite_scroll_pagination: ^0.1.0
|
hookified_infinite_scroll_pagination: ^0.1.0
|
||||||
@ -62,76 +40,44 @@ dependencies:
|
|||||||
hive: ^2.2.2
|
hive: ^2.2.2
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
dbus: ^0.7.3
|
dbus: ^0.7.3
|
||||||
audioplayers: ^1.0.1
|
audioplayers:
|
||||||
|
git:
|
||||||
|
url: https://github.com/bluefireteam/audioplayers.git
|
||||||
|
ref: 3ee12cd0361c0fc2f3d0303c504732d12fa8e49a
|
||||||
|
path: packages/audioplayers/
|
||||||
introduction_screen: ^3.0.2
|
introduction_screen: ^3.0.2
|
||||||
audio_session: ^0.1.9
|
audio_session: ^0.1.9
|
||||||
file_picker: ^4.6.1
|
# This is temporary until the win32v3 update PR is merged and released
|
||||||
|
file_picker:
|
||||||
|
git:
|
||||||
|
url: https://github.com/KRTirtho/flutter_file_picker.git
|
||||||
popover: ^0.2.6+3
|
popover: ^0.2.6+3
|
||||||
queue: ^3.1.0+1
|
queue: ^3.1.0+1
|
||||||
auto_size_text: ^3.0.0
|
auto_size_text: ^3.0.0
|
||||||
badges: ^2.0.3
|
badges: ^2.0.3
|
||||||
mime: ^1.0.2
|
mime: ^1.0.2
|
||||||
dart_tags: ^0.4.0
|
metadata_god: ^0.1.1
|
||||||
id3: ^1.0.2
|
|
||||||
mp3_info: ^0.2.0
|
# Temporary before [package_info_plus_windows] is updated to support
|
||||||
|
# win32v3
|
||||||
|
dependency_overrides:
|
||||||
|
win32: 3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
msix: ^2.8.0
|
|
||||||
|
|
||||||
# The "flutter_lints" package below contains a set of recommended lints to
|
|
||||||
# encourage good coding practices. The lint set provided by the package is
|
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
|
||||||
# package. See that file for information about deactivating specific lint
|
|
||||||
# rules and activating additional ones.
|
|
||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^1.0.0
|
||||||
flutter_launcher_icons: ^0.9.2
|
flutter_launcher_icons: ^0.9.2
|
||||||
hive_generator: ^1.1.3
|
hive_generator: ^1.1.3
|
||||||
build_runner: ^2.1.11
|
build_runner: ^2.1.11
|
||||||
flutter_distributor: ^0.0.2
|
flutter_distributor: ^0.0.2
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
|
||||||
|
|
||||||
# The following section is specific to Flutter.
|
|
||||||
flutter:
|
flutter:
|
||||||
# The following line ensures that the Material Icons font is
|
|
||||||
# included with your application, so that you can use the icons in
|
|
||||||
# the material Icons class.
|
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
|
||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
- assets/tutorial/
|
- assets/tutorial/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware.
|
|
||||||
|
|
||||||
# For details regarding adding assets from package dependencies, see
|
|
||||||
# https://flutter.dev/assets-and-images/#from-packages
|
|
||||||
|
|
||||||
# To add custom fonts to your application, add a fonts section here,
|
|
||||||
# in this "flutter" section. Each entry in this list should have a
|
|
||||||
# "family" key with the font family name, and a "fonts" key with a
|
|
||||||
# list giving the asset and other descriptors for the font. For
|
|
||||||
# example:
|
|
||||||
# fonts:
|
|
||||||
# - family: Schyler
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/Schyler-Regular.ttf
|
|
||||||
# - asset: fonts/Schyler-Italic.ttf
|
|
||||||
# style: italic
|
|
||||||
# - family: Trajan Pro
|
|
||||||
# fonts:
|
|
||||||
# - asset: fonts/TrajanPro.ttf
|
|
||||||
# - asset: fonts/TrajanPro_Bold.ttf
|
|
||||||
# weight: 700
|
|
||||||
#
|
|
||||||
# For details regarding fonts from package dependencies,
|
|
||||||
# see https://flutter.dev/custom-fonts/#from-packages
|
|
||||||
|
|
||||||
flutter_icons:
|
flutter_icons:
|
||||||
android: true
|
android: true
|
||||||
image_path: "assets/spotube-logo.png"
|
image_path: "assets/spotube-logo.png"
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
|
#include <metadata_god/metadata_god_plugin_c_api.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
|
MetadataGodPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("MetadataGodPluginCApi"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
audioplayers_windows
|
audioplayers_windows
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
|
metadata_god
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user