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 )