diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..4549cb5d
--- /dev/null
+++ b/.github/dependabot.yml
@@ -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"
diff --git a/.github/workflows/spotube-nightly.yml b/.github/workflows/spotube-nightly.yml
index 837e9348..b26eb1d8 100644
--- a/.github/workflows/spotube-nightly.yml
+++ b/.github/workflows/spotube-nightly.yml
@@ -39,6 +39,11 @@ jobs:
with:
name: Spotube-Linux-Bundle
path: dist/
+ - name: Setup upterm session
+ if: ${{ failure() }}
+ uses: lhotari/action-upterm@v1
+ with:
+ limit-access-to-actor: true
build_android:
runs-on: ubuntu-latest
@@ -63,6 +68,11 @@ jobs:
name: Spotube-Android-Bundle
path: |
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:
runs-on: windows-latest
@@ -109,3 +119,8 @@ jobs:
name: Spotube-Macos-Bundle
path: |
build/Spotube-macos-x86_64.dmg
+ - name: Setup upterm session
+ if: ${{ failure() }}
+ uses: lhotari/action-upterm@v1
+ with:
+ limit-access-to-actor: true
diff --git a/.github/workflows/spotube-release.yml b/.github/workflows/spotube-release.yml
index f446af7c..4f9d1528 100644
--- a/.github/workflows/spotube-release.yml
+++ b/.github/workflows/spotube-release.yml
@@ -1,10 +1,8 @@
name: Spotube Release
on:
- workflow_dispatch:
- inputs:
- tag:
- description: The tag to release
- required: true
+ release:
+ types:
+ - published
jobs:
publish_chocolatey:
@@ -16,18 +14,17 @@ jobs:
repository: KRTirtho/flutter_distributor
ref: deb-implementation
path: build/flutter_distributor
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # # Optionally strip `v` prefix
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
# Replace Version in files
- run: |
choco install sed make -y
- sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.tag }}/" windows/runner/Runner.rc
- sed -i "s/%{{SPOTUBE_VERSION}}%/${{ github.event.inputs.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 }}/" windows/runner/Runner.rc
+ sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" choco-struct/tools/VERIFICATION.txt
+ sed -i "s/%{{SPOTUBE_VERSION}}%/${{ steps.tag.outputs.tag }}/" choco-struct/spotube.nuspec
# Build Windows Executable
- uses: subosito/flutter-action@v2.2.0
@@ -67,11 +64,11 @@ jobs:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- # - name: Get latest tag
- # uses: dawidd6/action-get-tag@v1
- # id: tag
- # with:
- # strip_v: true
+ - name: Get latest tag
+ uses: dawidd6/action-get-tag@v1
+ id: tag
+ with:
+ strip_v: true
- uses: subosito/flutter-action@v2
with:
cache: true
@@ -82,23 +79,23 @@ jobs:
- run: du -sh build/macos/Build/Products/Release/spotube.app
- run: npm install -g appdmg
# using a versioned path for compatibility in gensums
- - run: mkdir -p build/${{ github.event.inputs.tag }}
- - run: appdmg appdmg.json build/${{ github.event.inputs.tag }}/Spotube-macos-x86_64.dmg
+ - run: mkdir -p build/${{ steps.tag.outputs.tag }}
+ - run: appdmg appdmg.json build/${{ steps.tag.outputs.tag }}/Spotube-macos-x86_64.dmg
- uses: actions/upload-artifact@v2
with:
name: Spotube-Macos-Bundle
path: |
- build/${{ github.event.inputs.tag }}/Spotube-macos-x86_64.dmg
+ build/${{ steps.tag.outputs.tag }}/Spotube-macos-x86_64.dmg
publish_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- uses: subosito/flutter-action@v2
with:
cache: true
@@ -114,7 +111,7 @@ jobs:
mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
# replacing & adding new release version with older version
- run: |
- sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml
+ sed -i 's|%{{APPDATA_RELEASE}}%||' linux/com.github.KRTirtho.Spotube.appdata.xml
- run: |
flutter config --enable-linux-desktop
@@ -136,11 +133,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- uses: subosito/flutter-action@v2
with:
cache: true
@@ -187,11 +184,11 @@ jobs:
with:
name: Spotube-Android-Bundle
path: ./Spotube-Android-Bundle
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- run: sudo apt-get install tree -y
# generating checksums for all the binary
- run: |
@@ -209,7 +206,7 @@ jobs:
- uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- tag: v${{ github.event.inputs.tag }}
+ tag: v${{ steps.tag.outputs.tag }}
omitBodyDuringUpdate: true
omitNameDuringUpdate: true
omitPrereleaseDuringUpdate: true
@@ -236,17 +233,17 @@ jobs:
- uses: actions/checkout@v3
with:
path: spotube
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- 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
- uses: EndBug/add-and-commit@v9
with:
- message: v${{ github.event.inputs.tag }} Update
+ message: v${{ steps.tag.outputs.tag }} Update
push: origin master
publish_aur:
@@ -254,17 +251,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- # - name: Get latest tag
- # id: tag
- # uses: dawidd6/action-get-tag@v1
- # with:
- # strip_v: true
+ - name: Get latest tag
+ id: tag
+ uses: dawidd6/action-get-tag@v1
+ with:
+ strip_v: true
- uses: actions/download-artifact@v3
with:
name: Spotube-Linux-Bundle
path: ./Spotube-Linux-Bundle
- 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/%{{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
@@ -274,4 +271,4 @@ jobs:
commit_username: ${{ secrets.AUR_USERNAME }}
commit_email: ${{ secrets.AUR_EMAIL }}
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 }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d5e16be..cd026aab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,59 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+
+## [2.4.1](https://github.com/KRTirtho/spotube/compare/v2.4.0...v2.4.1) (2022-09-13)
+
+
+### Features
+
+* add macos audio metadata tags support ([5866b0f](https://github.com/KRTirtho/spotube/commit/5866b0fcd661cf32060bb1485ea81634fbb9b90a))
+* remove macos bounds for reading and writing audio metadata ([16064f6](https://github.com/KRTirtho/spotube/commit/16064f68e882b091401ace4b895e387f46635800))
+* **search:** horizontal swipe scroll support for Desktop platform ([d5ff927](https://github.com/KRTirtho/spotube/commit/d5ff927c7273b6e72c5d775ee777f2cbd0d6d05c))
+
+
+### Bug Fixes
+
+* **artist-page:** SpotubeMarqueeText used in ArtistCard crashes the app ([4279541](https://github.com/KRTirtho/spotube/commit/427954150ab65b250e79fc844fc864abff5b6972))
+* **layout:** Fix adaptive UI not working correctly by providing a overriding option ([8c7adde](https://github.com/KRTirtho/spotube/commit/8c7adde890105e0267b71994b7928277f84553e5))
+* **local-track:** throwing exception when downloadLocation is empty ([1a3556d](https://github.com/KRTirtho/spotube/commit/1a3556d39e8473cadb6143192c48465dc6485599))
+
+## [2.4.0](https://github.com/KRTirtho/spotube/compare/v2.3.0...v2.4.0) (2022-09-09)
+
+
+### Features
+
+* Ability to change download location added ([816707c](https://github.com/KRTirtho/spotube/commit/816707c643f8d60d25bc08fd4c8005daa2ba9e63))
+* add download multi tracks support for mobile platform ([0476bf7](https://github.com/KRTirtho/spotube/commit/0476bf7ceece034a927d1df6099d8b33036f8a9b))
+* add download queue for desktop & initial playlist download support ([08f913e](https://github.com/KRTirtho/spotube/commit/08f913e9761d0f5c447af9dfb6eedb44b675498c))
+* add download tab on library ([8d77b69](https://github.com/KRTirtho/spotube/commit/8d77b6900a81aab020e19397e788964b0ac499ff))
+* add web support although nothing works just as expected ([2818ed5](https://github.com/KRTirtho/spotube/commit/2818ed5c9dadb9185a52762599c1dd0acd81e6bf))
+* **broken:** Broken Warning! Initial Local Audio Player ([c3bf511](https://github.com/KRTirtho/spotube/commit/c3bf5119ebb7c17e8c32f149598508674b0acd39))
+* **download:** track table view multi select improvement, tap to play track support, existing track replace confirmation dialog and bulk download confirmation dialog ([e217553](https://github.com/KRTirtho/spotube/commit/e21755322f2cd5f1fba00c5c8cd5c5d1f79e459d))
+* **local-tracks:** complete support for local tracks ([e206f16](https://github.com/KRTirtho/spotube/commit/e206f16723ac989ad58006c1b3c90c6691d8cab3))
+* **mpris:** MPRIS metadata are now updated in realtime ([d9addcd](https://github.com/KRTirtho/spotube/commit/d9addcda8e9562803bd73016148fab22560ee050))
+* **playback:** add repeat track support [#166](https://github.com/KRTirtho/spotube/issues/166) ([cae9993](https://github.com/KRTirtho/spotube/commit/cae99934299bd197c68f626d6c10158d449770b9))
+* **synced-lyrics:** animated active text size ([531fae6](https://github.com/KRTirtho/spotube/commit/531fae64f94b21551a7a0da363a9ab0d44f5d3b1))
+* **ui:** adaptive TrackTile actions & Setting ListTile ([615d5ce](https://github.com/KRTirtho/spotube/commit/615d5ce901eb0512e84a120b7309c9053238ee36))
+
+
+### Bug Fixes
+
+* **adaptive-list-tile:** dialog content not updating when content has changed ([a1d4230](https://github.com/KRTirtho/spotube/commit/a1d423090c854ebe319a0fa03fd6e5c4007b1387))
+* album & playlist card, player view and album view play button logic ([55852bd](https://github.com/KRTirtho/spotube/commit/55852bd15bc709d61fbba8cbea01ceca791d154c))
+* **docs:** indentions ([4a291d5](https://github.com/KRTirtho/spotube/commit/4a291d5f20dabe68f3ed64071624dcbed8327329))
+* **downloader:** downloaded track is corrupted for tagging ([2ab1fba](https://github.com/KRTirtho/spotube/commit/2ab1fba3d64147e3c5cf34756dce1cf6046d410a))
+* **downloader:** flutter downloader exception on desktop platform and too much width of TrackTile index no. ([d668760](https://github.com/KRTirtho/spotube/commit/d6687603d148ad936530cca4d09e128a59b79b19))
+* dropped flutter_downloader deps due to slow download speed and UserDownloads not showing for anonymous ([307a8e2](https://github.com/KRTirtho/spotube/commit/307a8e21df1e39123a1dca4c1b063eab50359581))
+* flutter_downloader manifest configuration breaking android support ([f3a0f78](https://github.com/KRTirtho/spotube/commit/f3a0f78fb92ff7ee38b5a9ef9954575d4282f954))
+* login screen not using safearea and no dialog bg-color found on light mode in AdaptivePopupMenuButton ([92bc611](https://github.com/KRTirtho/spotube/commit/92bc611c5e901dcabf34086be9287ac20317259a))
+* **performance:** always running marquee text causes high GPU usage [#175](https://github.com/KRTirtho/spotube/issues/175) and UserArtist overflow on smaller displays ([a23ce61](https://github.com/KRTirtho/spotube/commit/a23ce614467b4297f495b824f0958ff07c21ae92))
+* **playback:** shuffle button sometimes gets stuck and stops working [#183](https://github.com/KRTirtho/spotube/issues/183) ([4240433](https://github.com/KRTirtho/spotube/commit/4240433e3dde6ab948d2674e07e41c27c1f6eac8))
+* **player-overlay:** flickering when a track is changed or navigated to another page ([e48b67c](https://github.com/KRTirtho/spotube/commit/e48b67cd47ae54ad9268aead268e444836a67b0d))
+* **sidebar:** user image url ([747efc6](https://github.com/KRTirtho/spotube/commit/747efc6ee66bc6c7c917cc02bd134968a0781701))
+* **synced-lyrics:** active lyrics contrast ratio ([aba1ba9](https://github.com/KRTirtho/spotube/commit/aba1ba932592923720a36395c057f78820dafecf))
+* tabbar overflow in small screen, artist card too small title and synced lyrics contrast increased ([585de8c](https://github.com/KRTirtho/spotube/commit/585de8c1def9750826568317109b242a5e18f28c))
+
# v2.3.0
### New
diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart
index c439d609..b12b9687 100644
--- a/lib/components/Album/AlbumCard.dart
+++ b/lib/components/Album/AlbumCard.dart
@@ -21,7 +21,10 @@ class AlbumCard extends HookConsumerWidget {
final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
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()),
isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading &&
diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart
index 701aaa5f..adb6f188 100644
--- a/lib/components/Album/AlbumView.dart
+++ b/lib/components/Album/AlbumView.dart
@@ -27,7 +27,10 @@ class AlbumView extends HookConsumerWidget {
tracks: tracks,
id: album.id!,
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),
);
@@ -50,7 +53,10 @@ class AlbumView extends HookConsumerWidget {
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
final albumArt = useMemoized(
- () => TypeConversionUtils.image_X_UrlString(album.images),
+ () => TypeConversionUtils.image_X_UrlString(
+ album.images,
+ placeholder: ImagePlaceholder.albumArt,
+ ),
[album.images]);
final breakpoint = useBreakpoints();
@@ -69,6 +75,14 @@ class AlbumView extends HookConsumerWidget {
onPlay: ([track]) {
if (tracksSnapshot.asData?.value != null) {
if (!isAlbumPlaying) {
+ playPlaylist(
+ playback,
+ tracksSnapshot.asData!.value
+ .map((track) =>
+ TypeConversionUtils.simpleTrack_X_Track(track, album))
+ .toList(),
+ );
+ } else if (isAlbumPlaying && track != null) {
playPlaylist(
playback,
tracksSnapshot.asData!.value
diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart
index c6007274..cf13be86 100644
--- a/lib/components/Artist/ArtistCard.dart
+++ b/lib/components/Artist/ArtistCard.dart
@@ -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:go_router/go_router.dart';
import 'package:spotify/spotify.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 {
final Artist artist;
@@ -11,11 +12,12 @@ class ArtistCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final backgroundImage = CachedNetworkImageProvider((artist
- .images?.isNotEmpty ??
- false)
- ? artist.images!.first.url!
- : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6");
+ final backgroundImage = UniversalImage.imageProvider(
+ TypeConversionUtils.image_X_UrlString(
+ artist.images,
+ placeholder: ImagePlaceholder.artist,
+ ),
+ );
return SizedBox(
height: 240,
width: 200,
@@ -32,32 +34,55 @@ class ArtistCard extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
- blurRadius: 10,
- offset: const Offset(0, 3),
- spreadRadius: 5,
- color: Theme.of(context).shadowColor)
+ blurRadius: 10,
+ offset: const Offset(0, 3),
+ spreadRadius: 5,
+ color: Theme.of(context).shadowColor,
+ )
],
),
child: Padding(
padding: const EdgeInsets.all(15),
child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
- CircleAvatar(
- maxRadius: 80,
- minRadius: 20,
- backgroundImage: backgroundImage,
+ Stack(
+ children: [
+ CircleAvatar(
+ maxRadius: 80,
+ minRadius: 20,
+ backgroundImage: backgroundImage,
+ ),
+ Positioned(
+ right: 0,
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 10,
+ vertical: 5,
+ ),
+ decoration: BoxDecoration(
+ color: Colors.blue,
+ borderRadius: BorderRadius.circular(50)),
+ child: const Text(
+ "Artist",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ),
+ ],
),
- SpotubeMarqueeText(
- text: artist.name!,
+ AutoSizeText(
+ artist.name!,
+ maxLines: 2,
+ textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.bold,
),
- isHovering: isHovering,
),
- Text(
- "Artist",
- style: Theme.of(context).textTheme.subtitle1,
- )
],
),
),
diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart
index f5a94da8..7ace926d 100644
--- a/lib/components/Artist/ArtistProfile.dart
+++ b/lib/components/Artist/ArtistProfile.dart
@@ -1,4 +1,3 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/Shared/PageWindowTitleBar.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/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
@@ -78,8 +78,11 @@ class ArtistProfile extends HookConsumerWidget {
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
- backgroundImage: CachedNetworkImageProvider(
- TypeConversionUtils.image_X_UrlString(data.images),
+ backgroundImage: UniversalImage.imageProvider(
+ TypeConversionUtils.image_X_UrlString(
+ data.images,
+ placeholder: ImagePlaceholder.artist,
+ ),
),
),
Padding(
@@ -193,7 +196,9 @@ class ArtistProfile extends HookConsumerWidget {
id: data.id!,
name: "${data.name!} To Tracks",
thumbnail: TypeConversionUtils.image_X_UrlString(
- data.images),
+ data.images,
+ placeholder: ImagePlaceholder.artist,
+ ),
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
);
@@ -233,10 +238,10 @@ class ArtistProfile extends HookConsumerWidget {
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl =
TypeConversionUtils.image_X_UrlString(
- track.value.album?.images,
- index:
- (track.value.album?.images?.length ?? 1) -
- 1);
+ track.value.album?.images,
+ index: (track.value.album?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.albumArt,
+ );
return TrackTile(
playback,
duration: duration,
diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart
index 6084448e..efb2ddfe 100644
--- a/lib/components/Home/Home.dart
+++ b/lib/components/Home/Home.dart
@@ -12,7 +12,7 @@ import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerCategories.dart';
import 'package:spotube/components/Lyrics/SyncedLyrics.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/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart';
@@ -43,13 +43,14 @@ class Home extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- final int titleBarDragMaxWidth = useBreakpointValue(
- md: 80,
- lg: 256,
- sm: 0,
- xl: 256,
- xxl: 256,
+ final double titleBarWidth = useBreakpointValue(
+ sm: 0.0,
+ md: 80.0,
+ lg: 256.0,
+ xl: 256.0,
+ xxl: 256.0,
);
+ final extended = ref.watch(sidebarExtendedStateProvider);
final _selectedIndex = useState(0);
_onSelectedIndexChanged(int index) => _selectedIndex.value = index;
@@ -82,7 +83,9 @@ class Home extends HookConsumerWidget {
children: [
Container(
constraints: BoxConstraints(
- maxWidth: titleBarDragMaxWidth.toDouble(),
+ maxWidth: extended == null
+ ? titleBarWidth
+ : (extended ? 256 : 80),
),
color: Theme.of(context).navigationRailTheme.backgroundColor,
child: MoveWindow(),
@@ -111,6 +114,10 @@ class Home extends HookConsumerWidget {
}, [backgroundColor]);
return Scaffold(
+ bottomNavigationBar: SpotubeNavigationBar(
+ selectedIndex: _selectedIndex.value,
+ onSelectedIndexChanged: _onSelectedIndexChanged,
+ ),
body: Column(
children: [
if (_selectedIndex.value != 3)
@@ -178,10 +185,6 @@ class Home extends HookConsumerWidget {
),
// player itself
Player(),
- SpotubeNavigationBar(
- selectedIndex: _selectedIndex.value,
- onSelectedIndexChanged: _onSelectedIndexChanged,
- ),
],
),
);
diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart
index 6b2459c4..ed55873c 100644
--- a/lib/components/Home/Sidebar.dart
+++ b/lib/components/Home/Sidebar.dart
@@ -1,19 +1,21 @@
import 'package:badges/badges.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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/models/sideBarTiles.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
+import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
+final sidebarExtendedStateProvider = StateProvider((ref) => null);
+
class Sidebar extends HookConsumerWidget {
final int selectedIndex;
final void Function(int) onSelectedIndexChanged;
@@ -45,16 +47,15 @@ class Sidebar extends HookConsumerWidget {
final downloadCount = ref.watch(
downloaderProvider.select((s) => s.currentlyRunning),
);
-
- final int titleBarDragMaxWidth = useBreakpointValue(
- md: 80,
- lg: 256,
- sm: 0,
- xl: 256,
- xxl: 256,
- );
+ final forceExtended = ref.watch(sidebarExtendedStateProvider);
useEffect(() {
+ if (forceExtended != null) {
+ if (extended.value != forceExtended) {
+ extended.value = forceExtended;
+ }
+ return;
+ }
if (breakpoints.isMd && extended.value) {
extended.value = false;
} else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) &&
@@ -64,7 +65,17 @@ class Sidebar extends HookConsumerWidget {
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(
child: Material(
@@ -75,11 +86,11 @@ class Sidebar extends HookConsumerWidget {
if (selectedIndex == 3 && kIsDesktop)
SizedBox(
height: appWindow.titleBarHeight,
- width: titleBarDragMaxWidth.toDouble(),
+ width: extended.value ? 256 : 80,
child: MoveWindow(),
),
Padding(
- padding: const EdgeInsets.only(left: 15),
+ padding: const EdgeInsets.only(left: 10),
child: (extended.value)
? Row(
children: [
@@ -87,11 +98,25 @@ class Sidebar extends HookConsumerWidget {
const SizedBox(
width: 10,
),
- Text("Spotube",
- style: Theme.of(context).textTheme.headline4),
+ Text(
+ "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(
child: NavigationRail(
@@ -129,14 +154,16 @@ class Sidebar extends HookConsumerWidget {
),
),
SizedBox(
- width: titleBarDragMaxWidth.toDouble(),
+ width: extended.value ? 256 : 80,
child: Builder(
builder: (context) {
final data = meSnapshot.asData?.value;
final avatarImg = TypeConversionUtils.image_X_UrlString(
- data?.images,
- index: (data?.images?.length ?? 1) - 1);
+ data?.images,
+ index: (data?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.artist,
+ );
if (extended.value) {
return Padding(
padding: const EdgeInsets.all(16),
@@ -155,7 +182,8 @@ class Sidebar extends HookConsumerWidget {
children: [
CircleAvatar(
backgroundImage:
- CachedNetworkImageProvider(avatarImg),
+ UniversalImage.imageProvider(
+ avatarImg),
onBackgroundImageError:
(exception, stackTrace) =>
Image.asset(
@@ -193,7 +221,7 @@ class Sidebar extends HookConsumerWidget {
onTap: () => goToSettings(context),
child: CircleAvatar(
backgroundImage:
- CachedNetworkImageProvider(avatarImg),
+ UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",
diff --git a/lib/components/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart
index 562e3b40..ab376404 100644
--- a/lib/components/Home/SpotubeNavigationBar.dart
+++ b/lib/components/Home/SpotubeNavigationBar.dart
@@ -1,11 +1,11 @@
import 'package:badges/badges.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/Home/Sidebar.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/Downloader.dart';
+import 'package:spotube/provider/UserPreferences.dart';
class SpotubeNavigationBar extends HookConsumerWidget {
final int selectedIndex;
@@ -23,8 +23,12 @@ class SpotubeNavigationBar extends HookConsumerWidget {
downloaderProvider.select((s) => s.currentlyRunning),
);
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(
destinations: [
...sidebarTileList.map(
diff --git a/lib/components/Library/UserDownloads.dart b/lib/components/Library/UserDownloads.dart
index 916c0c95..c8bed68d 100644
--- a/lib/components/Library/UserDownloads.dart
+++ b/lib/components/Library/UserDownloads.dart
@@ -3,6 +3,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
+import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
@@ -53,11 +54,12 @@ class UserDownloads extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 5),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
- child: CachedNetworkImage(
+ child: UniversalImage(
height: 40,
width: 40,
- imageUrl: TypeConversionUtils.image_X_UrlString(
+ path: TypeConversionUtils.image_X_UrlString(
track.album?.images,
+ placeholder: ImagePlaceholder.albumArt,
),
),
),
diff --git a/lib/components/Library/UserLocalTracks.dart b/lib/components/Library/UserLocalTracks.dart
index 152b1a71..d481ecf1 100644
--- a/lib/components/Library/UserLocalTracks.dart
+++ b/lib/components/Library/UserLocalTracks.dart
@@ -1,12 +1,9 @@
-import 'dart:convert';
import 'dart:io';
-import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart';
-import 'package:mp3_info/mp3_info.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.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/utils/primitive_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
-import 'package:id3/id3.dart';
-
-final tagProcessor = TagProcessor();
const supportedAudioTypes = [
"audio/webm",
"audio/ogg",
"audio/mpeg",
+ "audio/mp4",
"audio/opus",
"audio/wav",
"audio/aac",
@@ -40,9 +35,11 @@ const imgMimeToExt = {
final localTracksProvider = FutureProvider>((ref) async {
try {
- final downloadDir = Directory(
- ref.watch(userPreferencesProvider.select((s) => s.downloadLocation)),
+ final downloadLocation = ref.watch(
+ userPreferencesProvider.select((s) => s.downloadLocation),
);
+ if (downloadLocation.isEmpty) return [];
+ final downloadDir = Directory(downloadLocation);
if (!await downloadDir.exists()) {
await downloadDir.create(recursive: true);
return [];
@@ -56,70 +53,39 @@ final localTracksProvider = FutureProvider>((ref) async {
}).map(
(f) async {
try {
- final bytes = f.readAsBytes();
- final mp3Instance = MP3Instance(await bytes);
+ final metadata = await MetadataGod.getMetadata(f);
- bool isParsed = false;
- try {
- isParsed = mp3Instance.parseTagsSync();
- } catch (e, stack) {
- getLogger(MP3Instance).e("[parseTagsSync]", e, stack);
- }
-
- final imageFile = isParsed
- ? File(join(
- (await getTemporaryDirectory()).path,
- "spotube",
- basenameWithoutExtension(f.path) +
- imgMimeToExt[mp3Instance.metaTags["APIC"]?["mime"] ??
- "image/jpeg"]!,
- ))
- : null;
- if (imageFile != null &&
- !await imageFile.exists() &&
- mp3Instance.metaTags["APIC"]?["base64"] != null) {
+ final imageFile = File(join(
+ (await getTemporaryDirectory()).path,
+ "spotube",
+ basenameWithoutExtension(f.path) +
+ imgMimeToExt[metadata?.picture?.mimeType ?? "image/jpeg"]!,
+ ));
+ if (!await imageFile.exists() && metadata?.picture != null) {
await imageFile.create(recursive: true);
await imageFile.writeAsBytes(
- base64Decode(
- mp3Instance.metaTags["APIC"]["base64"],
- ),
+ metadata?.picture?.data ?? [],
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,
- "duration": duration,
- };
+ return {"metadata": metadata, "file": f, "art": imageFile.path};
} catch (e, stack) {
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
- return {
- "metadata": [],
- "file": f,
- "duration": Duration.zero,
- };
+ return {};
}
},
),
- ));
+ ))
+ .where((e) => e.isNotEmpty)
+ .toList();
final tracks = filesWithMetadata
.map(
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
- fileWithMetadata["metadata"] as List,
- fileWithMetadata["file"] as File,
- fileWithMetadata["duration"] as Duration,
- fileWithMetadata["art"] as String?,
+ fileWithMetadata["file"],
+ metadata: fileWithMetadata["metadata"],
+ art: fileWithMetadata["art"],
),
)
.toList();
@@ -144,7 +110,10 @@ class UserLocalTracks extends HookConsumerWidget {
tracks: tracks,
id: "local",
name: "Local Tracks",
- thumbnail: TypeConversionUtils.image_X_UrlString(null),
+ thumbnail: TypeConversionUtils.image_X_UrlString(
+ null,
+ placeholder: ImagePlaceholder.collection,
+ ),
isLocal: true,
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
@@ -217,17 +186,11 @@ class UserLocalTracks extends HookConsumerWidget {
: "assets/album-placeholder.png",
isLocal: true,
onTrackPlayButtonPressed: (currentTrack) {
- if (tracks.isNotEmpty) {
- if (!isPlaylistPlaying) {
- playLocalTracks(
- playback,
- tracks,
- currentTrack: track,
- );
- } else {
- playback.stop();
- }
- }
+ return playLocalTracks(
+ playback,
+ tracks,
+ currentTrack: track,
+ );
},
);
},
diff --git a/lib/components/Login/LoginForm.dart b/lib/components/Login/LoginForm.dart
index c315b410..e2078b12 100644
--- a/lib/components/Login/LoginForm.dart
+++ b/lib/components/Login/LoginForm.dart
@@ -62,6 +62,16 @@ class LoginForm extends HookConsumerWidget {
const SizedBox(height: 20),
ElevatedButton(
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);
},
child: const Text("Submit"),
diff --git a/lib/components/Lyrics/SyncedLyrics.dart b/lib/components/Lyrics/SyncedLyrics.dart
index 1610460e..eebe336f 100644
--- a/lib/components/Lyrics/SyncedLyrics.dart
+++ b/lib/components/Lyrics/SyncedLyrics.dart
@@ -112,6 +112,7 @@ class SyncedLyrics extends HookConsumerWidget {
() => TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.albumArt,
),
[playback.track?.album?.images],
);
@@ -221,22 +222,23 @@ class SyncedLyrics extends HookConsumerWidget {
: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
- child: AutoSizeText(
- lyricSlice.text,
- maxLines: 2,
- style: Theme.of(context)
- .textTheme
- .headline4
- ?.copyWith(
- color: isActive
- ? Colors.white
- : palette.bodyTextColor,
- // indicating the active state of that lyric slice
- fontWeight: isActive
- ? FontWeight.bold
- : null,
- ),
- textAlign: TextAlign.center,
+ child: AnimatedDefaultTextStyle(
+ duration: const Duration(
+ milliseconds: 250),
+ style: TextStyle(
+ color: isActive
+ ? Colors.white
+ : palette.bodyTextColor,
+ fontWeight: isActive
+ ? FontWeight.bold
+ : FontWeight.normal,
+ fontSize: isActive ? 30 : 26,
+ ),
+ child: Text(
+ lyricSlice.text,
+ maxLines: 2,
+ textAlign: TextAlign.center,
+ ),
),
),
),
diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart
index 3a0e0832..5750e1b9 100644
--- a/lib/components/Player/Player.dart
+++ b/lib/components/Player/Player.dart
@@ -8,6 +8,7 @@ import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
+import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class Player extends HookConsumerWidget {
@@ -17,6 +18,8 @@ class Player extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
+ final layoutMode =
+ ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
final breakpoint = useBreakpoints();
@@ -25,6 +28,7 @@ class Player extends HookConsumerWidget {
? TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.albumArt,
)
: "assets/album-placeholder.png",
[playback.track?.album?.images],
@@ -50,7 +54,9 @@ class Player extends HookConsumerWidget {
WidgetsBinding.instance.addPostFrameCallback((time) {
// clearing the overlay-entry as passing the already available
// 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 &&
playback.track != null) {
entryRef.value = OverlayEntry(
@@ -74,11 +80,13 @@ class Player extends HookConsumerWidget {
return () {
disposeOverlay();
};
- }, [breakpoint, playback.track]);
+ }, [breakpoint, playback.track, layoutMode]);
// returning an empty non spacious Container as the overlay will take
// 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();
}
diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart
index ac92963d..08d03e15 100644
--- a/lib/components/Player/PlayerActions.dart
+++ b/lib/components/Player/PlayerActions.dart
@@ -1,16 +1,19 @@
import 'package:flutter/foundation.dart';
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/components/Player/PlayerQueue.dart';
-import 'package:spotube/components/Shared/DownloadTrackButton.dart';
import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/hooks/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart';
+import 'package:spotube/provider/Downloader.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/SpotifyRequests.dart';
+import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerActions extends HookConsumerWidget {
final MainAxisAlignment mainAxisAlignment;
@@ -27,7 +30,25 @@ class PlayerActions extends HookConsumerWidget {
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
final Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider);
+ final downloader = ref.watch(downloaderProvider);
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(
+ element.artists ?? []) ==
+ TypeConversionUtils.artists_X_String(
+ playback.track?.artists ?? []),
+ ) ==
+ true;
+ }, [localTracks, playback.track]);
+
return Row(
mainAxisAlignment: mainAxisAlignment,
children: [
@@ -56,9 +77,25 @@ class PlayerActions extends HookConsumerWidget {
: null,
),
if (!kIsWeb)
- DownloadTrackButton(
- track: playback.track,
- ),
+ if (isInQueue)
+ 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)
FutureBuilder(
future: playback.track?.id != null
diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart
index fcd556ed..b6096e86 100644
--- a/lib/components/Player/PlayerControls.dart
+++ b/lib/components/Player/PlayerControls.dart
@@ -31,25 +31,26 @@ class PlayerControls extends HookConsumerWidget {
child: Column(
children: [
StreamBuilder(
- stream: playback.player.onPositionChanged,
- builder: (context, snapshot) {
- final totalMinutes = PrimitiveUtils.zeroPadNumStr(
- duration.inMinutes.remainder(60));
- final totalSeconds = PrimitiveUtils.zeroPadNumStr(
- duration.inSeconds.remainder(60));
- final currentMinutes = snapshot.hasData
- ? PrimitiveUtils.zeroPadNumStr(
- snapshot.data!.inMinutes.remainder(60))
- : "00";
- final currentSeconds = snapshot.hasData
- ? PrimitiveUtils.zeroPadNumStr(
- snapshot.data!.inSeconds.remainder(60))
- : "00";
+ stream: playback.player.onPositionChanged,
+ builder: (context, snapshot) {
+ final totalMinutes = PrimitiveUtils.zeroPadNumStr(
+ duration.inMinutes.remainder(60));
+ final totalSeconds = PrimitiveUtils.zeroPadNumStr(
+ duration.inSeconds.remainder(60));
+ final currentMinutes = snapshot.hasData
+ ? PrimitiveUtils.zeroPadNumStr(
+ snapshot.data!.inMinutes.remainder(60))
+ : "00";
+ final currentSeconds = snapshot.hasData
+ ? PrimitiveUtils.zeroPadNumStr(
+ snapshot.data!.inSeconds.remainder(60))
+ : "00";
- final sliderMax = duration.inSeconds;
- final sliderValue = snapshot.data?.inSeconds ?? 0;
+ final sliderMax = duration.inSeconds;
+ final sliderValue = snapshot.data?.inSeconds ?? 0;
- return HookBuilder(builder: (context) {
+ return HookBuilder(
+ builder: (context) {
final progressStatic =
(sliderMax == 0 || sliderValue > sliderMax)
? 0
@@ -84,7 +85,9 @@ class PlayerControls extends HookConsumerWidget {
activeColor: iconColor,
),
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ padding: const EdgeInsets.symmetric(
+ horizontal: 8.0,
+ ),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -97,26 +100,25 @@ class PlayerControls extends HookConsumerWidget {
),
],
);
- });
- }),
+ },
+ );
+ },
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
- icon: const Icon(Icons.shuffle_rounded),
- color: playback.isShuffled
- ? Theme.of(context).primaryColor
- : iconColor,
- onPressed: () {
- if (playback.track == null || playback.playlist == null) {
- return;
- }
- try {
- playback.toggleShuffle();
- } catch (e, stack) {
- logger.e("onShuffle", e, stack);
- }
- }),
+ icon: Icon(
+ playback.isLoop
+ ? Icons.repeat_one_rounded
+ : playback.isShuffled
+ ? Icons.shuffle_rounded
+ : Icons.repeat_rounded,
+ ),
+ onPressed: playback.track == null || playback.playlist == null
+ ? null
+ : playback.cyclePlaybackMode,
+ ),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
color: iconColor,
@@ -125,7 +127,11 @@ class PlayerControls extends HookConsumerWidget {
}),
IconButton(
icon: playback.status == PlaybackStatus.loading
- ? const CircularProgressIndicator()
+ ? const SizedBox(
+ height: 20,
+ width: 20,
+ child: CircularProgressIndicator(),
+ )
: Icon(
playback.isPlaying
? Icons.pause_rounded
@@ -154,6 +160,7 @@ class PlayerControls extends HookConsumerWidget {
)
],
),
+ const SizedBox(height: 5)
],
));
}
diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart
index b5dd5cca..302ea109 100644
--- a/lib/components/Player/PlayerOverlay.dart
+++ b/lib/components/Player/PlayerOverlay.dart
@@ -8,6 +8,7 @@ import 'package:spotube/hooks/playback.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
import 'package:spotube/provider/Playback.dart';
+import 'package:spotube/provider/UserPreferences.dart';
class PlayerOverlay extends HookConsumerWidget {
final String albumArt;
@@ -21,6 +22,9 @@ class PlayerOverlay extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final breakpoint = useBreakpoints();
final paletteColor = usePaletteColor(albumArt, ref);
+ final layoutMode = ref.watch(
+ userPreferencesProvider.select((s) => s.layoutMode),
+ );
var isHome = GoRouter.of(context).location == "/";
final isAllowedPage = ["/playlist/", "/album/"].any(
@@ -36,8 +40,17 @@ class PlayerOverlay extends HookConsumerWidget {
return AnimatedPositioned(
duration: const Duration(milliseconds: 2500),
right: (breakpoint.isMd && !isAllowedPage ? 10 : 5),
- left: (breakpoint.isSm || isAllowedPage ? 5 : 90),
- bottom: (breakpoint.isSm && !isAllowedPage ? 63 : 10),
+ left: (layoutMode == LayoutMode.compact ||
+ (breakpoint.isSm && layoutMode == LayoutMode.adaptive) ||
+ isAllowedPage
+ ? 5
+ : 90),
+ bottom: (layoutMode == LayoutMode.compact && !isAllowedPage) ||
+ (breakpoint.isSm &&
+ layoutMode == LayoutMode.adaptive &&
+ !isAllowedPage)
+ ? 63
+ : 10,
child: GestureDetector(
onVerticalDragEnd: (details) {
int sensitivity = 8;
diff --git a/lib/components/Player/PlayerQueue.dart b/lib/components/Player/PlayerQueue.dart
index aafdbd59..e371a9ae 100644
--- a/lib/components/Player/PlayerQueue.dart
+++ b/lib/components/Player/PlayerQueue.dart
@@ -113,6 +113,7 @@ class PlayerQueue extends HookConsumerWidget {
duration: duration,
thumbnailUrl: TypeConversionUtils.image_X_UrlString(
track.value.album?.images,
+ placeholder: ImagePlaceholder.albumArt,
),
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async {
diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart
index 4a33c16d..d3aece58 100644
--- a/lib/components/Player/PlayerView.dart
+++ b/lib/components/Player/PlayerView.dart
@@ -42,6 +42,7 @@ class PlayerView extends HookConsumerWidget {
() => TypeConversionUtils.image_X_UrlString(
currentTrack?.album?.images,
index: (currentTrack?.album?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.albumArt,
),
[currentTrack?.album?.images],
);
diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart
index 1fe96c57..7827f51c 100644
--- a/lib/components/Playlist/PlaylistCard.dart
+++ b/lib/components/Playlist/PlaylistCard.dart
@@ -23,7 +23,10 @@ class PlaylistCard extends HookConsumerWidget {
return PlaybuttonCard(
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
title: playlist.name!,
- imageUrl: TypeConversionUtils.image_X_UrlString(playlist.images),
+ imageUrl: TypeConversionUtils.image_X_UrlString(
+ playlist.images,
+ placeholder: ImagePlaceholder.collection,
+ ),
isPlaying: isPlaylistPlaying && playback.isPlaying,
isLoading: playback.status == PlaybackStatus.loading && isPlaylistPlaying,
onTap: () {
@@ -56,7 +59,10 @@ class PlaylistCard extends HookConsumerWidget {
tracks: tracks,
id: playlist.id!,
name: playlist.name!,
- thumbnail: TypeConversionUtils.image_X_UrlString(playlist.images),
+ thumbnail: TypeConversionUtils.image_X_UrlString(
+ playlist.images,
+ placeholder: ImagePlaceholder.collection,
+ ),
),
);
},
diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart
index 00b3bd6f..868c8fa3 100644
--- a/lib/components/Playlist/PlaylistView.dart
+++ b/lib/components/Playlist/PlaylistView.dart
@@ -33,7 +33,10 @@ class PlaylistView extends HookConsumerWidget {
tracks: tracks,
id: playlist.id!,
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),
);
@@ -58,7 +61,10 @@ class PlaylistView extends HookConsumerWidget {
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
final titleImage = useMemoized(
- () => TypeConversionUtils.image_X_UrlString(playlist.images),
+ () => TypeConversionUtils.image_X_UrlString(
+ playlist.images,
+ placeholder: ImagePlaceholder.collection,
+ ),
[playlist.images]);
final color = usePaletteGenerator(
@@ -78,6 +84,11 @@ class PlaylistView extends HookConsumerWidget {
onPlay: ([track]) {
if (tracksSnapshot.asData?.value != null) {
if (!isPlaylistPlaying) {
+ playPlaylist(
+ playback,
+ tracksSnapshot.asData!.value,
+ );
+ } else if (isPlaylistPlaying && track != null) {
playPlaylist(
playback,
tracksSnapshot.asData!.value,
diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart
index 44188fef..b8762c4f 100644
--- a/lib/components/Search/Search.dart
+++ b/lib/components/Search/Search.dart
@@ -1,3 +1,4 @@
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -110,7 +111,9 @@ class Search extends HookConsumerWidget {
duration: duration,
thumbnailUrl:
TypeConversionUtils.image_X_UrlString(
- track.value.album?.images),
+ track.value.album?.images,
+ placeholder: ImagePlaceholder.albumArt,
+ ),
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async {
var isPlaylistPlaying =
@@ -126,6 +129,8 @@ class Search extends HookConsumerWidget {
thumbnail: TypeConversionUtils
.image_X_UrlString(
currentTrack.album?.images,
+ placeholder:
+ ImagePlaceholder.albumArt,
),
),
);
@@ -143,19 +148,28 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
- Scrollbar(
- controller: albumController,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
+ ScrollConfiguration(
+ behavior:
+ ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
+ ),
+ child: Scrollbar(
controller: albumController,
- child: Row(
- children: albums.map((album) {
- return AlbumCard(
- TypeConversionUtils.simpleAlbum_X_Album(
- album,
- ),
- );
- }).toList(),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ controller: albumController,
+ child: Row(
+ children: albums.map((album) {
+ return AlbumCard(
+ TypeConversionUtils.simpleAlbum_X_Album(
+ album,
+ ),
+ );
+ }).toList(),
+ ),
),
),
),
@@ -166,21 +180,30 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
- Scrollbar(
- controller: artistController,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
+ ScrollConfiguration(
+ behavior:
+ ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
+ ),
+ child: Scrollbar(
controller: artistController,
- child: Row(
- children: artists
- .map(
- (artist) => Container(
- margin: const EdgeInsets.symmetric(
- horizontal: 15),
- child: ArtistCard(artist),
- ),
- )
- .toList(),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ controller: artistController,
+ child: Row(
+ children: artists
+ .map(
+ (artist) => Container(
+ margin: const EdgeInsets.symmetric(
+ horizontal: 15),
+ child: ArtistCard(artist),
+ ),
+ )
+ .toList(),
+ ),
),
),
),
@@ -191,20 +214,30 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
- Scrollbar(
- scrollbarOrientation: breakpoint > Breakpoints.md
- ? ScrollbarOrientation.bottom
- : ScrollbarOrientation.top,
- controller: playlistController,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
+ ScrollConfiguration(
+ behavior:
+ ScrollConfiguration.of(context).copyWith(
+ dragDevices: {
+ PointerDeviceKind.touch,
+ PointerDeviceKind.mouse,
+ },
+ ),
+ child: Scrollbar(
+ scrollbarOrientation:
+ breakpoint > Breakpoints.md
+ ? ScrollbarOrientation.bottom
+ : ScrollbarOrientation.top,
controller: playlistController,
- child: Row(
- children: playlists
- .map(
- (playlist) => PlaylistCard(playlist),
- )
- .toList(),
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ controller: playlistController,
+ child: Row(
+ children: playlists
+ .map(
+ (playlist) => PlaylistCard(playlist),
+ )
+ .toList(),
+ ),
),
),
),
diff --git a/lib/components/Settings/About.dart b/lib/components/Settings/About.dart
index 6761fe3a..33b14396 100644
--- a/lib/components/Settings/About.dart
+++ b/lib/components/Settings/About.dart
@@ -24,9 +24,10 @@ class About extends HookWidget {
@override
Widget build(BuildContext context) {
final info = usePackageInfo(
- appName: "Spotube",
- packageName: "oss.krtirtho.Spotube",
- version: "2.3.0");
+ appName: "Spotube",
+ packageName: "oss.krtirtho.Spotube",
+ version: "2.4.1",
+ );
return ListTile(
leading: Icon(Icons.info_outline_rounded),
diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart
index cf7f45e2..0f2f789f 100644
--- a/lib/components/Settings/Settings.dart
+++ b/lib/components/Settings/Settings.dart
@@ -123,6 +123,40 @@ class Settings extends HookConsumerWidget {
style:
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(
+ 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(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Theme"),
diff --git a/lib/components/Shared/DownloadConfirmationDialog.dart b/lib/components/Shared/DownloadConfirmationDialog.dart
index 4d7f61fd..0d2093eb 100644
--- a/lib/components/Shared/DownloadConfirmationDialog.dart
+++ b/lib/components/Shared/DownloadConfirmationDialog.dart
@@ -1,5 +1,5 @@
-import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
+import 'package:spotube/components/Shared/UniversalImage.dart';
class DownloadConfirmationDialog extends StatelessWidget {
const DownloadConfirmationDialog({Key? key}) : super(key: key);
@@ -9,11 +9,11 @@ class DownloadConfirmationDialog extends StatelessWidget {
return AlertDialog(
contentPadding: const EdgeInsets.all(15),
title: Row(
- children: [
- const Text("Are you sure?"),
- const SizedBox(width: 10),
- CachedNetworkImage(
- imageUrl:
+ children: const [
+ Text("Are you sure?"),
+ SizedBox(width: 10),
+ UniversalImage(
+ path:
"https://c.tenor.com/kHcmsxlKHEAAAAAM/rock-one-eyebrow-raised-rock-staring.gif",
height: 40,
width: 40,
diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart
deleted file mode 100644
index 64b395f4..00000000
--- a/lib/components/Shared/DownloadTrackButton.dart
+++ /dev/null
@@ -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.idle);
- YoutubeExplode yt = useMemoized(() => YoutubeExplode());
-
- final outputFile = useState(null);
- String fileName =
- "${track?.name} - ${TypeConversionUtils.artists_X_String(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(
- 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);
- },
- )
- ],
- );
- }
-}
diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart
index 40ba479c..04c9dba9 100644
--- a/lib/components/Shared/PlaybuttonCard.dart
+++ b/lib/components/Shared/PlaybuttonCard.dart
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
+import 'package:spotube/components/Shared/UniversalImage.dart';
class PlaybuttonCard extends StatelessWidget {
final void Function()? onTap;
@@ -55,8 +56,8 @@ class PlaybuttonCard extends StatelessWidget {
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
- child: CachedNetworkImage(
- imageUrl: imageUrl,
+ child: UniversalImage(
+ path: imageUrl,
placeholder: (context, url) =>
Image.asset("assets/placeholder.png"),
),
diff --git a/lib/components/Shared/ReplaceDownloadedFileDialog.dart b/lib/components/Shared/ReplaceDownloadedFileDialog.dart
new file mode 100644
index 00000000..d9427411
--- /dev/null
+++ b/lib/components/Shared/ReplaceDownloadedFileDialog.dart
@@ -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);
+ },
+ )
+ ],
+ );
+ }
+}
diff --git a/lib/components/Shared/TrackCollectionView.dart b/lib/components/Shared/TrackCollectionView.dart
index 1e3b1a91..edcb3cc0 100644
--- a/lib/components/Shared/TrackCollectionView.dart
+++ b/lib/components/Shared/TrackCollectionView.dart
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.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/hooks/useCustomStatusBarColor.dart';
import 'package:spotube/hooks/usePaletteColor.dart';
@@ -175,9 +176,7 @@ class TrackCollectionView extends HookConsumerWidget {
const BoxConstraints(maxHeight: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
- child: CachedNetworkImage(
- imageUrl: titleImage,
- ),
+ child: UniversalImage(path: titleImage),
),
),
Column(
diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart
index 4edcbd88..ff9bbbab 100644
--- a/lib/components/Shared/TracksTableView.dart
+++ b/lib/components/Shared/TracksTableView.dart
@@ -149,6 +149,7 @@ class TracksTableView extends HookConsumerWidget {
String? thumbnailUrl = TypeConversionUtils.image_X_UrlString(
track.value.album?.images,
index: (track.value.album?.images?.length ?? 1) - 1,
+ placeholder: ImagePlaceholder.albumArt,
);
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
diff --git a/lib/extensions/yt-video-from-cache-track.dart b/lib/extensions/yt-video-from-cache-track.dart
index 1777d8cb..aee94dde 100644
--- a/lib/extensions/yt-video-from-cache-track.dart
+++ b/lib/extensions/yt-video-from-cache-track.dart
@@ -12,6 +12,7 @@ extension VideoFromCacheTrackExtension on Video {
cacheTrack.uploadDate != null
? DateTime.tryParse(cacheTrack.uploadDate!)
: null,
+ cacheTrack.uploadDate,
cacheTrack.publishDate != null
? DateTime.tryParse(cacheTrack.publishDate!)
: null,
@@ -69,6 +70,7 @@ extension VideoToJson on Video {
map["author"],
ChannelId(map["channelId"]),
DateTime.tryParse(map["uploadDate"]),
+ map["uploadDate"],
DateTime.tryParse(map["publishDate"]),
map["description"],
parseDuration(map["duration"]),
diff --git a/lib/main.dart b/lib/main.dart
index b3b45dfc..b2169978 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,7 +8,7 @@ import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/models/GoRouteDeclarations.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
diff --git a/lib/models/Id3Tags.dart b/lib/models/Id3Tags.dart
deleted file mode 100644
index c2d0f885..00000000
--- a/lib/models/Id3Tags.dart
+++ /dev/null
@@ -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 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 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 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 json) => Comment(
- json["lang"] ?? "",
- json["description"] ?? "",
- json["comment"] ?? "",
- );
-
- Map toJson() => {
- "comment": comment,
- "description": description,
- "key": key,
- "lang": lang,
- };
-}
-
-extension AttachedPictureJson on AttachedPicture {
- static fromJson(Map json) => AttachedPicture(
- json["mime"] ?? "",
- json["imageTypeCode"] ?? 0,
- json["description"] ?? "",
- List.from(json["imageData"] ?? []),
- );
-
- Map 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 json) {
- title = json['title'];
- artist = json['artist'];
- album = json['album'];
- year = json['year'];
- comment = json['comment'];
- track = json['track'];
- genre = json['genre'];
- }
-
- Map toJson() {
- return {
- 'title': title,
- 'artist': artist,
- 'album': album,
- 'year': year,
- 'comment': comment,
- 'track': track,
- 'genre': genre,
- };
- }
-}
diff --git a/lib/models/Logger.dart b/lib/models/Logger.dart
index d0a6288c..029f4c14 100644
--- a/lib/models/Logger.dart
+++ b/lib/models/Logger.dart
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
+import 'package:spotube/utils/platform.dart';
final _loggerFactory = _SpotubeLogger();
@@ -18,8 +19,11 @@ class _SpotubeLogger extends Logger {
@override
void log(Level level, message, [error, StackTrace? stackTrace]) {
- getApplicationDocumentsDirectory().then((dir) async {
- final file = File(path.join(dir.path, ".spotube_logs"));
+ (kIsAndroid
+ ? getExternalStorageDirectory()
+ : getApplicationDocumentsDirectory())
+ .then((dir) async {
+ final file = File(path.join(dir!.path, ".spotube_logs"));
if (level == Level.error) {
await file.writeAsString("[${DateTime.now()}]\n$message\n$stackTrace",
mode: FileMode.writeOnlyAppend);
diff --git a/lib/provider/Downloader.dart b/lib/provider/Downloader.dart
index c8f4f238..30596cc0 100644
--- a/lib/provider/Downloader.dart
+++ b/lib/provider/Downloader.dart
@@ -1,22 +1,18 @@
import 'dart:async';
-import 'dart:convert';
import 'dart:io';
-import 'package:dart_tags/dart_tags.dart';
-import 'package:flutter/widgets.dart';
+import 'package:flutter/widgets.dart' hide Image;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
+import 'package:metadata_god/metadata_god.dart';
import 'package:queue/queue.dart';
import 'package:path/path.dart' as path;
-import 'package:spotify/spotify.dart';
-import 'package:spotube/components/Library/UserLocalTracks.dart';
-import 'package:spotube/models/Id3Tags.dart';
+import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart';
-import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
@@ -63,7 +59,7 @@ class Downloader with ChangeNotifier {
RegExp(r'[/\\?%*:|"<>]'),
"",
);
- final filename = '$cleanTitle.mp3';
+ final filename = '$cleanTitle.m4a';
final file = File(path.join(downloadPath, filename));
try {
logger.v("[addToQueue] Download starting for ${file.path}");
@@ -97,54 +93,38 @@ class Downloader with ChangeNotifier {
"[addToQueue] Download of ${file.path} is done successfully",
);
- // Tagging isn't supported in Android currently
- if (kIsAndroid) return;
-
+ logger.v(
+ "[addToQueue] Writing metadata to ${file.path}",
+ );
final imageUri = TypeConversionUtils.image_X_UrlString(
track.album?.images ?? [],
+ placeholder: ImagePlaceholder.online,
);
- final response = await get(
- Uri.parse(
- imageUri,
- ),
- );
- final picture = AttachedPicture.base64(
- response.headers["Content-Type"] ?? "image/jpeg",
- 3,
- track.name!,
- base64Encode(response.bodyBytes),
- );
- // write id3 metadata
- final tag = Id3Tags(
- album: track.album?.name,
- picture: picture,
- title: track.name,
- genre: "Spotube",
- tcop: track.ytTrack.uploadDate?.year.toString(),
- tdrc: track.ytTrack.uploadDate?.year.toString(),
- tpe2: TypeConversionUtils.artists_X_String(
- track.artists ?? [],
- ),
- tsse: "",
- comment: Comment(
- "eng",
- track.ytTrack.description,
- track.ytTrack.title,
- ),
- );
+ final response = await get(Uri.parse(imageUri));
- 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 MetadataGod.writeMetadata(
+ file,
+ Metadata(
+ title: track.name,
+ artist: track.artists?.map((a) => a.name).join(", "),
+ album: track.album?.name,
+ albumArtist: track.artists?.map((a) => a.name).join(", "),
+ year: track.album?.releaseDate != null
+ ? int.tryParse(track.album!.releaseDate!)
+ : null,
+ trackNumber: track.trackNumber,
+ discNumber: track.discNumber,
+ durationMs: track.durationMs?.toDouble(),
+ fileSize: file.lengthSync(),
+ trackTotal: track.album?.tracks?.length,
+ picture: response.headers['content-type'] != null
+ ? Image(
+ data: response.bodyBytes,
+ mimeType: response.headers['content-type']!,
+ )
+ : null,
+ ),
);
- await file.writeAsBytes(taggedMp3);
logger.v(
"[addToQueue] Writing metadata to ${file.path} is successful",
);
diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart
index 1f93260d..f5429208 100644
--- a/lib/provider/Playback.dart
+++ b/lib/provider/Playback.dart
@@ -37,9 +37,15 @@ enum AudioQuality {
low,
}
+enum PlaybackMode {
+ repeat,
+ shuffle,
+ normal,
+}
+
class Playback extends PersistedChangeNotifier {
// player properties
- bool isShuffled;
+ PlaybackMode playbackMode;
bool isPlaying;
Duration currentDuration;
double volume;
@@ -72,9 +78,9 @@ class Playback extends PersistedChangeNotifier {
required this.youtube,
required this.ref,
this.mobileAudioService,
- }) : volume = 0,
- isShuffled = false,
+ }) : volume = 1,
isPlaying = false,
+ playbackMode = PlaybackMode.normal,
currentDuration = Duration.zero,
_subscriptions = [],
status = PlaybackStatus.idle,
@@ -93,6 +99,10 @@ class Playback extends PersistedChangeNotifier {
await player.setVolume(volume);
}
+ addListener(() {
+ _linuxAudioService?.player.updateProperties(this);
+ });
+
_subscriptions.addAll([
player.onPlayerStateChanged.listen(
(state) async {
@@ -102,7 +112,13 @@ class Playback extends PersistedChangeNotifier {
),
player.onPlayerComplete.listen((_) {
if (track?.id != null) {
- seekForward();
+ if (isLoop) {
+ final prevTrack = track;
+ track = null;
+ play(prevTrack!);
+ } else if (playlist != null) {
+ seekForward();
+ }
} else {
isPlaying = false;
status = PlaybackStatus.idle;
@@ -210,6 +226,7 @@ class Playback extends PersistedChangeNotifier {
artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString(
track.album?.images,
+ placeholder: ImagePlaceholder.online,
),
),
duration: track.ytTrack.duration,
@@ -249,12 +266,26 @@ class Playback extends PersistedChangeNotifier {
isPlaying ? await pause() : await resume();
}
- toggleShuffle() {
- final result = isShuffled ? playlist?.unshuffle() : playlist?.shuffle();
- if (result == true) {
- isShuffled = !isShuffled;
- notifyListeners();
+ void cyclePlaybackMode() {
+ switch (playbackMode) {
+ case PlaybackMode.normal:
+ playbackMode = PlaybackMode.shuffle;
+ playlist?.shuffle();
+ break;
+ case PlaybackMode.shuffle:
+ playbackMode = PlaybackMode.repeat;
+ playlist?.unshuffle();
+ break;
+ case PlaybackMode.repeat:
+ playbackMode = PlaybackMode.normal;
+ break;
}
+ notifyListeners();
+ }
+
+ void setPlaybackMode(PlaybackMode mode) {
+ playbackMode = mode;
+ notifyListeners();
}
Future seekPosition(Duration position) {
@@ -272,7 +303,7 @@ class Playback extends PersistedChangeNotifier {
await player.stop();
await player.release();
isPlaying = false;
- isShuffled = false;
+ playbackMode = PlaybackMode.normal;
playlist = null;
track = null;
status = PlaybackStatus.idle;
@@ -558,6 +589,10 @@ class Playback extends PersistedChangeNotifier {
"volume": volume,
};
}
+
+ bool get isLoop => playbackMode == PlaybackMode.repeat;
+ bool get isShuffled => playbackMode == PlaybackMode.shuffle;
+ bool get isNormal => playbackMode == PlaybackMode.normal;
}
final playbackProvider = ChangeNotifierProvider((ref) {
diff --git a/lib/provider/SpotifyRequests.dart b/lib/provider/SpotifyRequests.dart
index 84a1efb3..b46a1f65 100644
--- a/lib/provider/SpotifyRequests.dart
+++ b/lib/provider/SpotifyRequests.dart
@@ -133,7 +133,10 @@ final currentUserQuery = FutureProvider(
Image()
..height = 50
..width = 50
- ..url = TypeConversionUtils.image_X_UrlString(me.images),
+ ..url = TypeConversionUtils.image_X_UrlString(
+ me.images,
+ placeholder: ImagePlaceholder.artist,
+ ),
];
}
return me;
diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart
index 3f9f0020..3b4f2906 100644
--- a/lib/provider/UserPreferences.dart
+++ b/lib/provider/UserPreferences.dart
@@ -1,5 +1,4 @@
import 'dart:async';
-import 'dart:io';
import 'package:flutter/material.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:path/path.dart' as path;
+enum LayoutMode {
+ compact,
+ extended,
+ adaptive,
+}
+
class UserPreferences extends PersistedChangeNotifier {
ThemeMode themeMode;
String ytSearchFormat;
@@ -30,11 +35,14 @@ class UserPreferences extends PersistedChangeNotifier {
String downloadLocation;
+ LayoutMode layoutMode;
+
UserPreferences({
required this.geniusAccessToken,
required this.recommendationMarket,
required this.themeMode,
required this.ytSearchFormat,
+ required this.layoutMode,
this.saveTrackLyrics = false,
this.accentColorScheme = Colors.green,
this.backgroundColorScheme = Colors.grey,
@@ -126,6 +134,12 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence();
}
+ void setLayoutMode(LayoutMode mode) {
+ layoutMode = mode;
+ notifyListeners();
+ updatePersistence();
+ }
+
Future _getDefaultDownloadDirectory() async {
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
return getDownloadsDirectory().then((dir) {
@@ -158,6 +172,11 @@ class UserPreferences extends PersistedChangeNotifier {
skipSponsorSegments = map["skipSponsorSegments"] ?? skipSponsorSegments;
downloadLocation =
map["downloadLocation"] ?? await _getDefaultDownloadDirectory();
+
+ layoutMode = LayoutMode.values.firstWhere(
+ (mode) => mode.name == map["layoutMode"],
+ orElse: () => kIsDesktop ? LayoutMode.extended : LayoutMode.compact,
+ );
}
@override
@@ -175,6 +194,7 @@ class UserPreferences extends PersistedChangeNotifier {
"audioQuality": audioQuality.index,
"skipSponsorSegments": skipSponsorSegments,
"downloadLocation": downloadLocation,
+ "layoutMode": layoutMode.name,
};
}
}
@@ -185,5 +205,6 @@ final userPreferencesProvider = ChangeNotifierProvider(
recommendationMarket: 'US',
themeMode: ThemeMode.system,
ytSearchFormat: "\$MAIN_ARTIST - \$TITLE \$FEATURED_ARTISTS",
+ layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive,
),
);
diff --git a/lib/services/LinuxAudioService.dart b/lib/services/LinuxAudioService.dart
index 29d56be9..0b6b2d73 100644
--- a/lib/services/LinuxAudioService.dart
+++ b/lib/services/LinuxAudioService.dart
@@ -217,7 +217,7 @@ class _MprisMediaPlayer2 extends DBusObject {
}
class _MprisMediaPlayer2Player extends DBusObject {
- final Playback playback;
+ Playback playback;
/// Creates a new object to expose on [path].
_MprisMediaPlayer2Player({
@@ -275,7 +275,9 @@ class _MprisMediaPlayer2Player extends DBusObject {
/// Sets property org.mpris.MediaPlayer2.Player.Shuffle
Future setShuffle(bool value) async {
- playback.toggleShuffle();
+ playback.setPlaybackMode(
+ value ? PlaybackMode.shuffle : PlaybackMode.normal,
+ );
return DBusMethodSuccessResponse();
}
@@ -298,7 +300,9 @@ class _MprisMediaPlayer2Player extends DBusObject {
"mpris:length": DBusInt32(playback.currentDuration.inMicroseconds),
"mpris:artUrl": DBusString(
TypeConversionUtils.image_X_UrlString(
- playback.track?.album?.images),
+ playback.track?.album?.images,
+ placeholder: ImagePlaceholder.albumArt,
+ ),
),
"xesam:album": DBusString(playback.track!.album!.name!),
"xesam:artist": DBusArray.string(
@@ -443,6 +447,30 @@ class _MprisMediaPlayer2Player extends DBusObject {
'org.mpris.MediaPlayer2.Player', 'Seeked', [DBusInt64(Position)]);
}
+ Future 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
List introspect() {
return [
diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart
index 315c24ee..574899c8 100644
--- a/lib/utils/service_utils.dart
+++ b/lib/utils/service_utils.dart
@@ -184,8 +184,12 @@ abstract class ServiceUtils {
mode: LaunchMode.externalApplication,
);
- HttpServer server =
- await HttpServer.bind(InternetAddress.loopbackIPv4, 4304);
+ HttpServer server = await HttpServer.bind(
+ InternetAddress.loopbackIPv4,
+ 4304,
+ shared: true,
+ );
+
logger.i("[connectIpc] Server started");
await for (HttpRequest request in server) {
diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart
index d887637a..7bf55cd2 100644
--- a/lib/utils/type_conversion_utils.dart
+++ b/lib/utils/type_conversion_utils.dart
@@ -2,22 +2,38 @@
import 'dart:io';
-import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/widgets.dart' hide Image;
+import 'package:metadata_god/metadata_god.dart' hide Image;
import 'package:path/path.dart';
import 'package:spotube/components/Shared/LinkText.dart';
import 'package:spotify/spotify.dart';
-import 'package:spotube/models/Id3Tags.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/utils/primitive_utils.dart';
-import 'package:collection/collection.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
+enum ImagePlaceholder {
+ albumArt,
+ artist,
+ collection,
+ online,
+}
+
abstract class TypeConversionUtils {
- static String image_X_UrlString(List? images, {int index = 0}) {
+ static String image_X_UrlString(
+ List? 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
? images[0].url!
- : "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png";
+ : placeholderUrl;
}
static String artists_X_String(List artists) {
@@ -91,31 +107,24 @@ abstract class TypeConversionUtils {
}
static SpotubeTrack localTrack_X_Track(
- List metadatas,
- File file,
- Duration duration,
+ File file, {
+ Metadata? metadata,
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(
Video(
VideoId("dQw4w9WgXcQ"),
basenameWithoutExtension(file.path),
- metadata.tpe2 ?? "",
+ metadata?.artist ?? "",
ChannelId(
"https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
),
DateTime.now(),
+ "",
DateTime.now(),
"",
- duration,
- ThumbnailSet(metadata.title ?? ""),
+ Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
+ ThumbnailSet(metadata?.title ?? ""),
[],
const Engagement(0, 0, 0),
false,
@@ -124,27 +133,28 @@ abstract class TypeConversionUtils {
[],
);
track.album = Album()
- ..name = metadata.album ?? "Spotube"
+ ..name = metadata?.album ?? "Spotube"
..images = [if (art != null) Image()..url = art]
- ..genres = [if (metadata.genre != null) metadata.genre!]
+ ..genres = [if (metadata?.genre != null) metadata!.genre!]
..artists = [
Artist()
- ..name = metadata.tpe2 ?? "Spotube"
- ..id = metadata.tpe2 ?? "Spotube"
+ ..name = metadata?.albumArtist ?? "Spotube"
+ ..id = metadata?.albumArtist ?? "Spotube"
..type = "artist",
]
- ..id = metadata.album;
+ ..id = metadata?.album
+ ..releaseDate = metadata?.year?.toString();
track.artists = [
Artist()
- ..name = metadata.tpe2 ?? "Spotube"
- ..id = metadata.tpe2 ?? "Spotube"
+ ..name = metadata?.artist ?? "Spotube"
+ ..id = metadata?.artist ?? "Spotube"
];
- track.id = metadata.title ?? basenameWithoutExtension(file.path);
- track.name = metadata.title ?? basenameWithoutExtension(file.path);
+ track.id = metadata?.title ?? basenameWithoutExtension(file.path);
+ track.name = metadata?.title ?? basenameWithoutExtension(file.path);
track.type = "track";
track.uri = file.path;
- track.durationMs = duration.inMilliseconds;
+ track.durationMs = metadata?.durationMs?.toInt();
return track;
}
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 01b8e0f7..bf0cd78b 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
void fl_register_plugins(FlPluginRegistry* registry) {
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 9aebc645..0de3bf80 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
bitsdojo_window_linux
+ metadata_god
url_launcher_linux
)
diff --git a/pubspec.lock b/pubspec.lock
index a0dcd9c8..b76961f4 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -15,13 +15,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
- ansicolor:
- dependency: transitive
- description:
- name: ansicolor
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.1"
app_package_maker:
dependency: transitive
description:
@@ -78,48 +71,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -172,9 +123,11 @@ packages:
audioplayers:
dependency: "direct main"
description:
- name: audioplayers
- url: "https://pub.dartlang.org"
- source: hosted
+ path: "packages/audioplayers"
+ ref: "3ee12cd0361c0fc2f3d0303c504732d12fa8e49a"
+ resolved-ref: "3ee12cd0361c0fc2f3d0303c504732d12fa8e49a"
+ url: "https://github.com/bluefireteam/audioplayers.git"
+ source: git
version: "1.0.1"
audioplayers_android:
dependency: transitive
@@ -238,21 +191,21 @@ packages:
name: bitsdojo_window
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.2"
+ version: "0.1.5"
bitsdojo_window_linux:
dependency: transitive
description:
name: bitsdojo_window_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.2"
+ version: "0.1.3"
bitsdojo_window_macos:
dependency: transitive
description:
name: bitsdojo_window_macos
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.2"
+ version: "0.1.3"
bitsdojo_window_platform_interface:
dependency: transitive
description:
@@ -266,7 +219,7 @@ packages:
name: bitsdojo_window_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.2"
+ version: "0.1.5"
boolean_selector:
dependency: transitive
description:
@@ -386,20 +339,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -435,27 +374,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
name: dbus
url: "https://pub.dartlang.org"
source: hosted
- version: "0.7.3"
- dio:
- dependency: transitive
- description:
- name: dio
- url: "https://pub.dartlang.org"
- source: hosted
- version: "4.0.6"
+ version: "0.7.8"
dots_indicator:
dependency: transitive
description:
@@ -483,7 +408,7 @@ packages:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
- version: "1.2.1"
+ version: "2.0.1"
file:
dependency: transitive
description:
@@ -494,10 +419,12 @@ packages:
file_picker:
dependency: "direct main"
description:
- name: file_picker
- url: "https://pub.dartlang.org"
- source: hosted
- version: "4.6.1"
+ path: "."
+ ref: HEAD
+ resolved-ref: f9133f6d5dbf33191fc9b58655aebfd15445045a
+ url: "https://github.com/KRTirtho/flutter_file_picker.git"
+ source: git
+ version: "5.0.1"
fixnum:
dependency: transitive
description:
@@ -524,13 +451,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -551,7 +471,7 @@ packages:
name: flutter_distributor
url: "https://pub.dartlang.org"
source: hosted
- version: "0.0.9"
+ version: "0.0.2"
flutter_hooks:
dependency: "direct main"
description:
@@ -587,6 +507,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct dev"
description: flutter
@@ -603,7 +530,7 @@ packages:
name: freezed_annotation
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "2.1.0"
frontend_server_client:
dependency: transitive
description:
@@ -695,20 +622,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
- id3:
- dependency: "direct main"
- description:
- name: id3
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.0.2"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
- version: "3.1.3"
+ version: "3.2.0"
infinite_scroll_pagination:
dependency: transitive
description:
@@ -793,6 +713,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
@@ -800,20 +727,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -841,7 +754,7 @@ packages:
name: package_info_plus
url: "https://pub.dartlang.org"
source: hosted
- version: "1.4.2"
+ version: "1.4.3+1"
package_info_plus_linux:
dependency: transitive
description:
@@ -876,7 +789,7 @@ packages:
name: package_info_plus_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.5"
+ version: "2.0.0"
palette_generator:
dependency: "direct main"
description:
@@ -884,13 +797,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: "direct main"
description:
@@ -904,7 +810,7 @@ packages:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.10"
+ version: "2.0.11"
path_provider_android:
dependency: transitive
description:
@@ -925,7 +831,7 @@ packages:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
- version: "2.1.6"
+ version: "2.1.7"
path_provider_macos:
dependency: transitive
description:
@@ -946,7 +852,7 @@ packages:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.6"
+ version: "2.1.3"
pedantic:
dependency: transitive
description:
@@ -1186,12 +1092,10 @@ packages:
spotify:
dependency: "direct main"
description:
- path: "."
- ref: HEAD
- resolved-ref: ea313e2d21c38157cd8255d248bcd7897bf51360
- url: "https://github.com/KRTirtho/spotify-dart.git"
- source: git
- version: "0.7.0"
+ name: spotify
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.8.0"
sqflite:
dependency: transitive
description:
@@ -1332,13 +1236,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -1375,12 +1272,12 @@ packages:
source: hosted
version: "2.2.0"
win32:
- dependency: transitive
+ dependency: "direct overridden"
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
- version: "2.6.1"
+ version: "3.0.0"
xdg_directories:
dependency: transitive
description:
@@ -1394,7 +1291,7 @@ packages:
name: xml
url: "https://pub.dartlang.org"
source: hosted
- version: "5.4.1"
+ version: "6.1.0"
yaml:
dependency: transitive
description:
@@ -1408,7 +1305,7 @@ packages:
name: youtube_explode_dart
url: "https://pub.dartlang.org"
source: hosted
- version: "1.11.0"
+ version: "1.12.0"
sdks:
dart: ">=2.17.1 <3.0.0"
flutter: ">=3.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 9ea0fc31..811f3f0e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,47 +1,25 @@
name: spotube
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
-# 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
+publish_to: "none"
-# The following defines the version and build number for your application.
-# 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
+version: 2.4.1+14
environment:
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:
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
cached_network_image: ^3.2.0
html: ^0.15.0
http: ^0.13.4
shared_preferences: ^2.0.11
- spotify:
- git: https://github.com/KRTirtho/spotify-dart.git
+ spotify: ^0.8.0
url_launcher: ^6.0.17
youtube_explode_dart: ^1.10.8
- bitsdojo_window: ^0.1.2
+ bitsdojo_window: ^0.1.5
path: ^1.8.0
path_provider: ^2.0.8
collection: ^1.15.0
@@ -54,7 +32,7 @@ dependencies:
permission_handler: ^9.2.0
marquee: ^2.2.1
scroll_to_index: ^2.1.1
- package_info_plus: ^1.4.2
+ package_info_plus: ^1.4.3
version: ^2.0.0
audio_service: ^0.18.4
hookified_infinite_scroll_pagination: ^0.1.0
@@ -62,76 +40,44 @@ dependencies:
hive: ^2.2.2
hive_flutter: ^1.1.0
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
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
queue: ^3.1.0+1
auto_size_text: ^3.0.0
badges: ^2.0.3
mime: ^1.0.2
- dart_tags: ^0.4.0
- id3: ^1.0.2
- mp3_info: ^0.2.0
+ metadata_god: ^0.1.1
+
+# Temporary before [package_info_plus_windows] is updated to support
+# win32v3
+dependency_overrides:
+ win32: 3.0.0
dev_dependencies:
flutter_test:
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_launcher_icons: ^0.9.2
hive_generator: ^1.1.3
build_runner: ^2.1.11
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:
- # 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
-
- # To add assets to your application, add an assets section, like this:
assets:
- assets/
- 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:
android: true
image_path: "assets/spotube-logo.png"
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 3e689c38..cf86bfe4 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
#include
@@ -16,6 +17,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
+ MetadataGodPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("MetadataGodPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index c8e970a8..76747345 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
bitsdojo_window_windows
+ metadata_god
permission_handler_windows
url_launcher_windows
)