mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Merge branch 'KRTirtho:master' into feature_duration_matching
This commit is contained in:
commit
cf1a8eff2a
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
|
||||
enable-beta-ecosystems: true
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
15
.github/workflows/spotube-nightly.yml
vendored
15
.github/workflows/spotube-nightly.yml
vendored
@ -39,6 +39,11 @@ jobs:
|
||||
with:
|
||||
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
|
||||
|
103
.github/workflows/spotube-release.yml
vendored
103
.github/workflows/spotube-release.yml
vendored
@ -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}}%|<release version="${{ github.event.inputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
|
||||
sed -i 's|%{{APPDATA_RELEASE}}%|<release version="${{ steps.tag.outputs.tag }}" date="${{ steps.date.outputs.date }}" />|' linux/com.github.KRTirtho.Spotube.appdata.xml
|
||||
|
||||
- run: |
|
||||
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 }}
|
||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@ -1,3 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [2.4.1](https://github.com/KRTirtho/spotube/compare/v2.4.0...v2.4.1) (2022-09-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add macos audio metadata tags support ([5866b0f](https://github.com/KRTirtho/spotube/commit/5866b0fcd661cf32060bb1485ea81634fbb9b90a))
|
||||
* remove macos bounds for reading and writing audio metadata ([16064f6](https://github.com/KRTirtho/spotube/commit/16064f68e882b091401ace4b895e387f46635800))
|
||||
* **search:** horizontal swipe scroll support for Desktop platform ([d5ff927](https://github.com/KRTirtho/spotube/commit/d5ff927c7273b6e72c5d775ee777f2cbd0d6d05c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **artist-page:** SpotubeMarqueeText used in ArtistCard crashes the app ([4279541](https://github.com/KRTirtho/spotube/commit/427954150ab65b250e79fc844fc864abff5b6972))
|
||||
* **layout:** Fix adaptive UI not working correctly by providing a overriding option ([8c7adde](https://github.com/KRTirtho/spotube/commit/8c7adde890105e0267b71994b7928277f84553e5))
|
||||
* **local-track:** throwing exception when downloadLocation is empty ([1a3556d](https://github.com/KRTirtho/spotube/commit/1a3556d39e8473cadb6143192c48465dc6485599))
|
||||
|
||||
## [2.4.0](https://github.com/KRTirtho/spotube/compare/v2.3.0...v2.4.0) (2022-09-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Ability to change download location added ([816707c](https://github.com/KRTirtho/spotube/commit/816707c643f8d60d25bc08fd4c8005daa2ba9e63))
|
||||
* add download multi tracks support for mobile platform ([0476bf7](https://github.com/KRTirtho/spotube/commit/0476bf7ceece034a927d1df6099d8b33036f8a9b))
|
||||
* add download queue for desktop & initial playlist download support ([08f913e](https://github.com/KRTirtho/spotube/commit/08f913e9761d0f5c447af9dfb6eedb44b675498c))
|
||||
* add download tab on library ([8d77b69](https://github.com/KRTirtho/spotube/commit/8d77b6900a81aab020e19397e788964b0ac499ff))
|
||||
* add web support although nothing works just as expected ([2818ed5](https://github.com/KRTirtho/spotube/commit/2818ed5c9dadb9185a52762599c1dd0acd81e6bf))
|
||||
* **broken:** Broken Warning! Initial Local Audio Player ([c3bf511](https://github.com/KRTirtho/spotube/commit/c3bf5119ebb7c17e8c32f149598508674b0acd39))
|
||||
* **download:** track table view multi select improvement, tap to play track support, existing track replace confirmation dialog and bulk download confirmation dialog ([e217553](https://github.com/KRTirtho/spotube/commit/e21755322f2cd5f1fba00c5c8cd5c5d1f79e459d))
|
||||
* **local-tracks:** complete support for local tracks ([e206f16](https://github.com/KRTirtho/spotube/commit/e206f16723ac989ad58006c1b3c90c6691d8cab3))
|
||||
* **mpris:** MPRIS metadata are now updated in realtime ([d9addcd](https://github.com/KRTirtho/spotube/commit/d9addcda8e9562803bd73016148fab22560ee050))
|
||||
* **playback:** add repeat track support [#166](https://github.com/KRTirtho/spotube/issues/166) ([cae9993](https://github.com/KRTirtho/spotube/commit/cae99934299bd197c68f626d6c10158d449770b9))
|
||||
* **synced-lyrics:** animated active text size ([531fae6](https://github.com/KRTirtho/spotube/commit/531fae64f94b21551a7a0da363a9ab0d44f5d3b1))
|
||||
* **ui:** adaptive TrackTile actions & Setting ListTile ([615d5ce](https://github.com/KRTirtho/spotube/commit/615d5ce901eb0512e84a120b7309c9053238ee36))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **adaptive-list-tile:** dialog content not updating when content has changed ([a1d4230](https://github.com/KRTirtho/spotube/commit/a1d423090c854ebe319a0fa03fd6e5c4007b1387))
|
||||
* album & playlist card, player view and album view play button logic ([55852bd](https://github.com/KRTirtho/spotube/commit/55852bd15bc709d61fbba8cbea01ceca791d154c))
|
||||
* **docs:** indentions ([4a291d5](https://github.com/KRTirtho/spotube/commit/4a291d5f20dabe68f3ed64071624dcbed8327329))
|
||||
* **downloader:** downloaded track is corrupted for tagging ([2ab1fba](https://github.com/KRTirtho/spotube/commit/2ab1fba3d64147e3c5cf34756dce1cf6046d410a))
|
||||
* **downloader:** flutter downloader exception on desktop platform and too much width of TrackTile index no. ([d668760](https://github.com/KRTirtho/spotube/commit/d6687603d148ad936530cca4d09e128a59b79b19))
|
||||
* dropped flutter_downloader deps due to slow download speed and UserDownloads not showing for anonymous ([307a8e2](https://github.com/KRTirtho/spotube/commit/307a8e21df1e39123a1dca4c1b063eab50359581))
|
||||
* flutter_downloader manifest configuration breaking android support ([f3a0f78](https://github.com/KRTirtho/spotube/commit/f3a0f78fb92ff7ee38b5a9ef9954575d4282f954))
|
||||
* login screen not using safearea and no dialog bg-color found on light mode in AdaptivePopupMenuButton ([92bc611](https://github.com/KRTirtho/spotube/commit/92bc611c5e901dcabf34086be9287ac20317259a))
|
||||
* **performance:** always running marquee text causes high GPU usage [#175](https://github.com/KRTirtho/spotube/issues/175) and UserArtist overflow on smaller displays ([a23ce61](https://github.com/KRTirtho/spotube/commit/a23ce614467b4297f495b824f0958ff07c21ae92))
|
||||
* **playback:** shuffle button sometimes gets stuck and stops working [#183](https://github.com/KRTirtho/spotube/issues/183) ([4240433](https://github.com/KRTirtho/spotube/commit/4240433e3dde6ab948d2674e07e41c27c1f6eac8))
|
||||
* **player-overlay:** flickering when a track is changed or navigated to another page ([e48b67c](https://github.com/KRTirtho/spotube/commit/e48b67cd47ae54ad9268aead268e444836a67b0d))
|
||||
* **sidebar:** user image url ([747efc6](https://github.com/KRTirtho/spotube/commit/747efc6ee66bc6c7c917cc02bd134968a0781701))
|
||||
* **synced-lyrics:** active lyrics contrast ratio ([aba1ba9](https://github.com/KRTirtho/spotube/commit/aba1ba932592923720a36395c057f78820dafecf))
|
||||
* tabbar overflow in small screen, artist card too small title and synced lyrics contrast increased ([585de8c](https://github.com/KRTirtho/spotube/commit/585de8c1def9750826568317109b242a5e18f28c))
|
||||
|
||||
# v2.3.0
|
||||
|
||||
### New
|
||||
|
@ -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 &&
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
@ -35,29 +37,52 @@ class ArtistCard extends StatelessWidget {
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
spreadRadius: 5,
|
||||
color: Theme.of(context).shadowColor)
|
||||
color: Theme.of(context).shadowColor,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
maxRadius: 80,
|
||||
minRadius: 20,
|
||||
backgroundImage: backgroundImage,
|
||||
),
|
||||
SpotubeMarqueeText(
|
||||
text: artist.name!,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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),
|
||||
);
|
||||
@ -234,9 +239,9 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
String? thumbnailUrl =
|
||||
TypeConversionUtils.image_X_UrlString(
|
||||
track.value.album?.images,
|
||||
index:
|
||||
(track.value.album?.images?.length ?? 1) -
|
||||
1);
|
||||
index: (track.value.album?.images?.length ?? 1) - 1,
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
);
|
||||
return TrackTile(
|
||||
playback,
|
||||
duration: duration,
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -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<bool?>((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);
|
||||
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",
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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<List<Track>>((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<List<Track>>((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(
|
||||
final imageFile = 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) {
|
||||
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": <Tag>[],
|
||||
"file": f,
|
||||
"duration": Duration.zero,
|
||||
};
|
||||
return {};
|
||||
}
|
||||
},
|
||||
),
|
||||
));
|
||||
))
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
final tracks = filesWithMetadata
|
||||
.map(
|
||||
(fileWithMetadata) => TypeConversionUtils.localTrack_X_Track(
|
||||
fileWithMetadata["metadata"] as List<Tag>,
|
||||
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(
|
||||
return playLocalTracks(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
} else {
|
||||
playback.stop();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@ -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"),
|
||||
|
@ -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,25 +222,26 @@ 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(
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: const Duration(
|
||||
milliseconds: 250),
|
||||
style: TextStyle(
|
||||
color: isActive
|
||||
? Colors.white
|
||||
: palette.bodyTextColor,
|
||||
// indicating the active state of that lyric slice
|
||||
fontWeight: isActive
|
||||
? FontWeight.bold
|
||||
: null,
|
||||
: FontWeight.normal,
|
||||
fontSize: isActive ? 30 : 26,
|
||||
),
|
||||
child: Text(
|
||||
lyricSlice.text,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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<Artist>(
|
||||
element.artists ?? []) ==
|
||||
TypeConversionUtils.artists_X_String<Artist>(
|
||||
playback.track?.artists ?? []),
|
||||
) ==
|
||||
true;
|
||||
}, [localTracks, playback.track]);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
children: [
|
||||
@ -56,8 +77,24 @@ 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<bool>(
|
||||
|
@ -49,7 +49,8 @@ class PlayerControls extends HookConsumerWidget {
|
||||
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)
|
||||
],
|
||||
));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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],
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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,7 +148,15 @@ class Search extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Scrollbar(
|
||||
ScrollConfiguration(
|
||||
behavior:
|
||||
ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: albumController,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@ -159,6 +172,7 @@ class Search extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (artists.isNotEmpty)
|
||||
Text(
|
||||
@ -166,7 +180,15 @@ class Search extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Scrollbar(
|
||||
ScrollConfiguration(
|
||||
behavior:
|
||||
ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: artistController,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
@ -184,6 +206,7 @@ class Search extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (playlists.isNotEmpty)
|
||||
Text(
|
||||
@ -191,8 +214,17 @@ class Search extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Scrollbar(
|
||||
scrollbarOrientation: breakpoint > Breakpoints.md
|
||||
ScrollConfiguration(
|
||||
behavior:
|
||||
ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
child: Scrollbar(
|
||||
scrollbarOrientation:
|
||||
breakpoint > Breakpoints.md
|
||||
? ScrollbarOrientation.bottom
|
||||
: ScrollbarOrientation.top,
|
||||
controller: playlistController,
|
||||
@ -208,6 +240,7 @@ class Search extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -26,7 +26,8 @@ class About extends HookWidget {
|
||||
final info = usePackageInfo(
|
||||
appName: "Spotube",
|
||||
packageName: "oss.krtirtho.Spotube",
|
||||
version: "2.3.0");
|
||||
version: "2.4.1",
|
||||
);
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(Icons.info_outline_rounded),
|
||||
|
@ -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<LayoutMode>(
|
||||
value: preferences.layoutMode,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
child: Text(
|
||||
"Adaptive",
|
||||
),
|
||||
value: LayoutMode.adaptive,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
child: Text(
|
||||
"Compact",
|
||||
),
|
||||
value: LayoutMode.compact,
|
||||
),
|
||||
DropdownMenuItem(
|
||||
child: Text("Extended"),
|
||||
value: LayoutMode.extended,
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
preferences.setLayoutMode(value);
|
||||
update?.call(() {});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
AdaptiveListTile(
|
||||
leading: const Icon(Icons.dark_mode_outlined),
|
||||
title: const Text("Theme"),
|
||||
|
@ -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,
|
||||
|
@ -1,228 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/models/SpotubeTrack.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
enum TrackStatus { downloading, idle, done }
|
||||
|
||||
class DownloadTrackButton extends HookConsumerWidget {
|
||||
final Track? track;
|
||||
const DownloadTrackButton({Key? key, this.track}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final UserPreferences preferences = ref.watch(userPreferencesProvider);
|
||||
final Playback playback = ref.watch(playbackProvider);
|
||||
final status = useState<TrackStatus>(TrackStatus.idle);
|
||||
YoutubeExplode yt = useMemoized(() => YoutubeExplode());
|
||||
|
||||
final outputFile = useState<File?>(null);
|
||||
String fileName =
|
||||
"${track?.name} - ${TypeConversionUtils.artists_X_String<Artist>(track?.artists ?? [])}";
|
||||
|
||||
useEffect(() {
|
||||
(() async {
|
||||
outputFile.value =
|
||||
File(path.join(preferences.downloadLocation, "$fileName.mp3"));
|
||||
}());
|
||||
return null;
|
||||
}, [fileName, track, preferences.downloadLocation]);
|
||||
|
||||
final _downloadTrack = useCallback(() async {
|
||||
try {
|
||||
if (track == null || outputFile.value == null) return;
|
||||
if ((kIsMobile) &&
|
||||
!await Permission.storage.isGranted &&
|
||||
!await Permission.storage.isPermanentlyDenied) {
|
||||
final status = await Permission.storage.request();
|
||||
if (!status.isGranted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content:
|
||||
Text("Couldn't download track. Not enough permissions"),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
StreamManifest manifest = await yt.videos.streamsClient
|
||||
.getManifest((track as SpotubeTrack).ytTrack.url);
|
||||
|
||||
File outputLyricsFile = File(
|
||||
path.join(preferences.downloadLocation, "$fileName-lyrics.txt"));
|
||||
|
||||
if (await outputFile.value!.exists()) {
|
||||
final shouldReplace = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ReplaceDownloadedFileDialog(track: track!);
|
||||
},
|
||||
);
|
||||
if (shouldReplace != true) return;
|
||||
}
|
||||
|
||||
final audioStream = yt.videos.streamsClient
|
||||
.get(
|
||||
manifest.audioOnly
|
||||
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
||||
.withHighestBitrate(),
|
||||
)
|
||||
.asBroadcastStream();
|
||||
|
||||
final statusCb = audioStream.listen(
|
||||
(event) {
|
||||
if (status.value != TrackStatus.downloading) {
|
||||
status.value = TrackStatus.downloading;
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
status.value = TrackStatus.done;
|
||||
ref.refresh(localTracksProvider);
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
if (status.value == TrackStatus.done) {
|
||||
status.value = TrackStatus.idle;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!await outputFile.value!.exists()) {
|
||||
await outputFile.value!.create(recursive: true);
|
||||
}
|
||||
|
||||
IOSink outputFileStream = outputFile.value!.openWrite();
|
||||
await audioStream.pipe(outputFileStream);
|
||||
await outputFileStream.flush();
|
||||
await outputFileStream.close().then((value) async {
|
||||
if (status.value == TrackStatus.downloading) {
|
||||
status.value = TrackStatus.done;
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
if (status.value == TrackStatus.done) {
|
||||
status.value = TrackStatus.idle;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return statusCb.cancel();
|
||||
});
|
||||
|
||||
if (preferences.saveTrackLyrics && playback.track != null) {
|
||||
if (!await outputLyricsFile.exists()) {
|
||||
await outputLyricsFile.create(recursive: true);
|
||||
}
|
||||
final lyrics = await ServiceUtils.getLyrics(
|
||||
playback.track!.name!,
|
||||
playback.track!.artists
|
||||
?.map((s) => s.name)
|
||||
.whereNotNull()
|
||||
.toList() ??
|
||||
[],
|
||||
apiKey: preferences.geniusAccessToken,
|
||||
optimizeQuery: true,
|
||||
);
|
||||
if (lyrics != null) {
|
||||
await outputLyricsFile.writeAsString(
|
||||
"$lyrics\n\nPowered by genius.com",
|
||||
mode: FileMode.writeOnly,
|
||||
);
|
||||
}
|
||||
}
|
||||
} on FileSystemException catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.red,
|
||||
content: Text("Download Failed. ${e.message} ${e.path}"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [
|
||||
track,
|
||||
status,
|
||||
yt,
|
||||
preferences.saveTrackLyrics,
|
||||
playback.track,
|
||||
outputFile.value,
|
||||
preferences.downloadLocation,
|
||||
fileName
|
||||
]);
|
||||
|
||||
useEffect(() {
|
||||
return () => yt.close();
|
||||
}, []);
|
||||
|
||||
final outputFileExists = useMemoized(
|
||||
() => outputFile.value?.existsSync() == true,
|
||||
[outputFile.value, status.value, track],
|
||||
);
|
||||
|
||||
if (status.value == TrackStatus.downloading) {
|
||||
return const SizedBox(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
height: 20,
|
||||
width: 20,
|
||||
);
|
||||
} else if (status.value == TrackStatus.done) {
|
||||
return const Icon(Icons.download_done_rounded);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
outputFileExists ? Icons.download_done_rounded : Icons.download_rounded,
|
||||
),
|
||||
onPressed: track != null &&
|
||||
track is SpotubeTrack &&
|
||||
playback.playlist?.isLocal != true
|
||||
? _downloadTrack
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ReplaceDownloadedFileDialog extends StatelessWidget {
|
||||
final Track track;
|
||||
const ReplaceDownloadedFileDialog({required this.track, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("Track ${track.name} Already Exists"),
|
||||
content:
|
||||
const Text("Do you want to replace the already downloaded track?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("No"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("Yes"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package: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"),
|
||||
),
|
||||
|
31
lib/components/Shared/ReplaceDownloadedFileDialog.dart
Normal file
31
lib/components/Shared/ReplaceDownloadedFileDialog.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class ReplaceDownloadedFileDialog extends StatelessWidget {
|
||||
final Track track;
|
||||
const ReplaceDownloadedFileDialog({required this.track, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text("Track ${track.name} Already Exists"),
|
||||
content:
|
||||
const Text("Do you want to replace the already downloaded track?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("No"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text("Yes"),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/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(
|
||||
|
@ -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)}";
|
||||
|
@ -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"]),
|
||||
|
@ -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';
|
||||
|
@ -1,157 +0,0 @@
|
||||
import 'package:dart_tags/dart_tags.dart';
|
||||
|
||||
class Id3Tags {
|
||||
Id3Tags({
|
||||
this.tsse,
|
||||
this.title,
|
||||
this.album,
|
||||
this.tpe2,
|
||||
this.comment,
|
||||
this.tcop,
|
||||
this.tdrc,
|
||||
this.genre,
|
||||
this.picture,
|
||||
});
|
||||
|
||||
String? tsse;
|
||||
String? title;
|
||||
String? album;
|
||||
String? tpe2;
|
||||
Comment? comment;
|
||||
String? tcop;
|
||||
String? tdrc;
|
||||
String? genre;
|
||||
AttachedPicture? picture;
|
||||
|
||||
factory Id3Tags.fromJson(Map<String, dynamic> json) => Id3Tags(
|
||||
tsse: json["TSSE"],
|
||||
title: json["title"],
|
||||
album: json["album"],
|
||||
tpe2: json["TPE2"],
|
||||
comment: json["comment"]?["eng:"] is Comment
|
||||
? json["comment"]["eng:"]
|
||||
: CommentJson.fromJson(Map.from(
|
||||
json["comment"]?["eng:"] ?? {},
|
||||
)),
|
||||
tcop: json["TCOP"],
|
||||
tdrc: json["TDRC"],
|
||||
genre: json["genre"],
|
||||
picture: json["picture"]?["Cover (front)"] is AttachedPicture
|
||||
? json["picture"]["Cover (front)"]
|
||||
: AttachedPictureJson.fromJson(Map.from(
|
||||
json["picture"]?["Cover (front)"] ?? {},
|
||||
)),
|
||||
);
|
||||
|
||||
factory Id3Tags.fromId3v1Tags(Id3v1Tags v1tags) => Id3Tags(
|
||||
album: v1tags.album,
|
||||
comment: Comment("", "", v1tags.comment ?? ""),
|
||||
genre: v1tags.genre,
|
||||
title: v1tags.title,
|
||||
tcop: v1tags.year,
|
||||
tdrc: v1tags.year,
|
||||
tpe2: v1tags.artist,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"TSSE": tsse,
|
||||
"title": title,
|
||||
"album": album,
|
||||
"TPE2": tpe2,
|
||||
"comment": comment,
|
||||
"TCOP": tcop,
|
||||
"TDRC": tdrc,
|
||||
"genre": genre,
|
||||
"picture": picture,
|
||||
};
|
||||
|
||||
String? get artist => tpe2;
|
||||
String? get year => tdrc;
|
||||
|
||||
Map<String, String> toAndroidJson(String artwork) {
|
||||
return {
|
||||
"title": title ?? "Unknown",
|
||||
"artist": artist ?? "Unknown",
|
||||
"album": album ?? "Unknown",
|
||||
"genre": genre ?? "Unknown",
|
||||
"artwork": artwork,
|
||||
"year": year ?? "Unknown",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension CommentJson on Comment {
|
||||
static fromJson(Map<String, dynamic> json) => Comment(
|
||||
json["lang"] ?? "",
|
||||
json["description"] ?? "",
|
||||
json["comment"] ?? "",
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"comment": comment,
|
||||
"description": description,
|
||||
"key": key,
|
||||
"lang": lang,
|
||||
};
|
||||
}
|
||||
|
||||
extension AttachedPictureJson on AttachedPicture {
|
||||
static fromJson(Map<String, dynamic> json) => AttachedPicture(
|
||||
json["mime"] ?? "",
|
||||
json["imageTypeCode"] ?? 0,
|
||||
json["description"] ?? "",
|
||||
List<int>.from(json["imageData"] ?? []),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"description": description,
|
||||
"imageData": imageData,
|
||||
"imageData64": imageData64,
|
||||
"imageType": imageType,
|
||||
"imageTypeCode": imageTypeCode,
|
||||
"key": key,
|
||||
"mime": mime,
|
||||
};
|
||||
}
|
||||
|
||||
class Id3v1Tags {
|
||||
String? title;
|
||||
String? artist;
|
||||
String? album;
|
||||
String? year;
|
||||
String? comment;
|
||||
String? track;
|
||||
String? genre;
|
||||
|
||||
Id3v1Tags({
|
||||
this.title,
|
||||
this.artist,
|
||||
this.album,
|
||||
this.year,
|
||||
this.comment,
|
||||
this.track,
|
||||
this.genre,
|
||||
});
|
||||
|
||||
Id3v1Tags.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
artist = json['artist'];
|
||||
album = json['album'];
|
||||
year = json['year'];
|
||||
comment = json['comment'];
|
||||
track = json['track'];
|
||||
genre = json['genre'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'title': title,
|
||||
'artist': artist,
|
||||
'album': album,
|
||||
'year': year,
|
||||
'comment': comment,
|
||||
'track': track,
|
||||
'genre': genre,
|
||||
};
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package: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);
|
||||
|
@ -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,
|
||||
final response = await get(Uri.parse(imageUri));
|
||||
|
||||
await MetadataGod.writeMetadata(
|
||||
file,
|
||||
Metadata(
|
||||
title: track.name,
|
||||
genre: "Spotube",
|
||||
tcop: track.ytTrack.uploadDate?.year.toString(),
|
||||
tdrc: track.ytTrack.uploadDate?.year.toString(),
|
||||
tpe2: TypeConversionUtils.artists_X_String<Artist>(
|
||||
track.artists ?? [],
|
||||
),
|
||||
tsse: "",
|
||||
comment: Comment(
|
||||
"eng",
|
||||
track.ytTrack.description,
|
||||
track.ytTrack.title,
|
||||
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,
|
||||
),
|
||||
);
|
||||
|
||||
logger.v("[addToQueue] Writing metadata to ${file.path}");
|
||||
|
||||
final taggedMp3 = await tagProcessor.putTagsToByteArray(
|
||||
file.readAsBytes(),
|
||||
[
|
||||
Tag()
|
||||
..type = "ID3"
|
||||
..version = "2.4.0"
|
||||
..tags = tag.toJson()
|
||||
],
|
||||
);
|
||||
await file.writeAsBytes(taggedMp3);
|
||||
logger.v(
|
||||
"[addToQueue] Writing metadata to ${file.path} is successful",
|
||||
);
|
||||
|
@ -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) {
|
||||
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;
|
||||
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<void> 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<Playback>((ref) {
|
||||
|
@ -133,7 +133,10 @@ final currentUserQuery = FutureProvider<User>(
|
||||
Image()
|
||||
..height = 50
|
||||
..width = 50
|
||||
..url = TypeConversionUtils.image_X_UrlString(me.images),
|
||||
..url = TypeConversionUtils.image_X_UrlString(
|
||||
me.images,
|
||||
placeholder: ImagePlaceholder.artist,
|
||||
),
|
||||
];
|
||||
}
|
||||
return me;
|
||||
|
@ -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<String> _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,
|
||||
),
|
||||
);
|
||||
|
@ -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<DBusMethodResponse> 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<void> updateProperties(Playback playback) async {
|
||||
this.playback = playback;
|
||||
return emitPropertiesChanged(
|
||||
"org.mpris.MediaPlayer2.Player",
|
||||
changedProperties: {
|
||||
"PlaybackStatus": (await getPlaybackStatus()).returnValues.first,
|
||||
"LoopStatus": (await getLoopStatus()).returnValues.first,
|
||||
"Rate": (await getRate()).returnValues.first,
|
||||
"Shuffle": (await getShuffle()).returnValues.first,
|
||||
"Metadata": (await getMetadata()).returnValues.first,
|
||||
"Volume": (await getVolume()).returnValues.first,
|
||||
"Position": (await getPosition()).returnValues.first,
|
||||
"MinimumRate": (await getMinimumRate()).returnValues.first,
|
||||
"MaximumRate": (await getMaximumRate()).returnValues.first,
|
||||
"CanGoNext": (await getCanGoNext()).returnValues.first,
|
||||
"CanGoPrevious": (await getCanGoPrevious()).returnValues.first,
|
||||
"CanPlay": (await getCanPlay()).returnValues.first,
|
||||
"CanPause": (await getCanPause()).returnValues.first,
|
||||
"CanSeek": (await getCanSeek()).returnValues.first,
|
||||
"CanControl": (await getCanControl()).returnValues.first,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<DBusIntrospectInterface> introspect() {
|
||||
return [
|
||||
|
@ -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) {
|
||||
|
@ -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<Image>? images, {int index = 0}) {
|
||||
static String image_X_UrlString(
|
||||
List<Image>? images, {
|
||||
int index = 0,
|
||||
required ImagePlaceholder placeholder,
|
||||
}) {
|
||||
final String placeholderUrl = {
|
||||
ImagePlaceholder.albumArt: "assets/album-placeholder.png",
|
||||
ImagePlaceholder.artist: "assets/user-placeholder.png",
|
||||
ImagePlaceholder.collection: "assets/placeholder.png",
|
||||
ImagePlaceholder.online:
|
||||
"https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png",
|
||||
}[placeholder]!;
|
||||
return images != null && images.isNotEmpty
|
||||
? images[0].url!
|
||||
: "https://avatars.dicebear.com/api/bottts/${PrimitiveUtils.uuid.v4()}.png";
|
||||
: placeholderUrl;
|
||||
}
|
||||
|
||||
static String artists_X_String<T extends ArtistSimple>(List<T> artists) {
|
||||
@ -91,31 +107,24 @@ abstract class TypeConversionUtils {
|
||||
}
|
||||
|
||||
static SpotubeTrack localTrack_X_Track(
|
||||
List<Tag> 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;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <audioplayers_linux/audioplayers_linux_plugin.h>
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
#include <metadata_god/metadata_god_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
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);
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_linux
|
||||
bitsdojo_window_linux
|
||||
metadata_god
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
197
pubspec.lock
197
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"
|
||||
|
94
pubspec.yaml
94
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"
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <audioplayers_windows/audioplayers_windows_plugin.h>
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <metadata_god/metadata_god_plugin_c_api.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
@ -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(
|
||||
|
@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
audioplayers_windows
|
||||
bitsdojo_window_windows
|
||||
metadata_god
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user