Merge branch 'KRTirtho:master' into feature_duration_matching

This commit is contained in:
Demizo 2022-09-18 12:34:59 -05:00 committed by GitHub
commit cf1a8eff2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 898 additions and 1019 deletions

14
.github/dependabot.yml vendored Normal file
View 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
@ -35,29 +37,52 @@ class ArtistCard extends StatelessWidget {
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: [
Stack(
children: [ children: [
CircleAvatar( CircleAvatar(
maxRadius: 80, maxRadius: 80,
minRadius: 20, minRadius: 20,
backgroundImage: backgroundImage, backgroundImage: backgroundImage,
), ),
SpotubeMarqueeText( Positioned(
text: artist.name!, 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,
),
),
),
),
],
),
AutoSizeText(
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,
)
], ],
), ),
), ),

View File

@ -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),
); );
@ -234,9 +239,9 @@ class ArtistProfile extends HookConsumerWidget {
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,

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
isParsed = mp3Instance.parseTagsSync();
} catch (e, stack) {
getLogger(MP3Instance).e("[parseTagsSync]", e, stack);
}
final imageFile = isParsed
? File(join(
(await getTemporaryDirectory()).path, (await getTemporaryDirectory()).path,
"spotube", "spotube",
basenameWithoutExtension(f.path) + basenameWithoutExtension(f.path) +
imgMimeToExt[mp3Instance.metaTags["APIC"]?["mime"] ?? imgMimeToExt[metadata?.picture?.mimeType ?? "image/jpeg"]!,
"image/jpeg"]!, ));
)) if (!await imageFile.exists() && metadata?.picture != null) {
: 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) {
playLocalTracks(
playback, playback,
tracks, tracks,
currentTrack: track, currentTrack: track,
); );
} else {
playback.stop();
}
}
}, },
); );
}, },

View File

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

View File

@ -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,25 +222,26 @@ 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
.headline4
?.copyWith(
color: isActive color: isActive
? Colors.white ? Colors.white
: palette.bodyTextColor, : palette.bodyTextColor,
// indicating the active state of that lyric slice
fontWeight: isActive fontWeight: isActive
? FontWeight.bold ? FontWeight.bold
: null, : FontWeight.normal,
fontSize: isActive ? 30 : 26,
), ),
child: Text(
lyricSlice.text,
maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
), ),
),
); );
}, },
), ),

View File

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

View File

@ -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,8 +77,24 @@ 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>(

View File

@ -49,7 +49,8 @@ class PlayerControls extends HookConsumerWidget {
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)
], ],
)); ));
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +148,15 @@ 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(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController, controller: albumController,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -159,6 +172,7 @@ class Search extends HookConsumerWidget {
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
if (artists.isNotEmpty) if (artists.isNotEmpty)
Text( Text(
@ -166,7 +180,15 @@ 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(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController, controller: artistController,
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -184,6 +206,7 @@ class Search extends HookConsumerWidget {
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
if (playlists.isNotEmpty) if (playlists.isNotEmpty)
Text( Text(
@ -191,8 +214,17 @@ 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:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom ? ScrollbarOrientation.bottom
: ScrollbarOrientation.top, : ScrollbarOrientation.top,
controller: playlistController, controller: playlistController,
@ -208,6 +240,7 @@ class Search extends HookConsumerWidget {
), ),
), ),
), ),
),
], ],
), ),
), ),

View File

@ -26,7 +26,8 @@ class About extends HookWidget {
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),

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, await MetadataGod.writeMetadata(
), file,
); Metadata(
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, title: track.name,
genre: "Spotube", artist: track.artists?.map((a) => a.name).join(", "),
tcop: track.ytTrack.uploadDate?.year.toString(), album: track.album?.name,
tdrc: track.ytTrack.uploadDate?.year.toString(), albumArtist: track.artists?.map((a) => a.name).join(", "),
tpe2: TypeConversionUtils.artists_X_String<Artist>( year: track.album?.releaseDate != null
track.artists ?? [], ? int.tryParse(track.album!.releaseDate!)
), : null,
tsse: "", trackNumber: track.trackNumber,
comment: Comment( discNumber: track.discNumber,
"eng", durationMs: track.durationMs?.toDouble(),
track.ytTrack.description, fileSize: file.lengthSync(),
track.ytTrack.title, trackTotal: track.album?.tracks?.length,
picture: response.headers['content-type'] != null
? Image(
data: response.bodyBytes,
mimeType: response.headers['content-type']!,
)
: null,
), ),
); );
logger.v("[addToQueue] Writing metadata to ${file.path}");
final taggedMp3 = await tagProcessor.putTagsToByteArray(
file.readAsBytes(),
[
Tag()
..type = "ID3"
..version = "2.4.0"
..tags = tag.toJson()
],
);
await file.writeAsBytes(taggedMp3);
logger.v( logger.v(
"[addToQueue] Writing metadata to ${file.path} is successful", "[addToQueue] Writing metadata to ${file.path} is successful",
); );

View File

@ -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) {
if (isLoop) {
final prevTrack = track;
track = null;
play(prevTrack!);
} else if (playlist != null) {
seekForward(); 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;
playlist?.shuffle();
break;
case PlaybackMode.shuffle:
playbackMode = PlaybackMode.repeat;
playlist?.unshuffle();
break;
case PlaybackMode.repeat:
playbackMode = PlaybackMode.normal;
break;
}
notifyListeners(); 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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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