Merge branch 'KRTirtho:master' into feature_duration_matching

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

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"

View File

@ -39,6 +39,11 @@ jobs:
with:
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

View File

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

View File

@ -1,3 +1,59 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.4.1](https://github.com/KRTirtho/spotube/compare/v2.4.0...v2.4.1) (2022-09-13)
### Features
* add macos audio metadata tags support ([5866b0f](https://github.com/KRTirtho/spotube/commit/5866b0fcd661cf32060bb1485ea81634fbb9b90a))
* remove macos bounds for reading and writing audio metadata ([16064f6](https://github.com/KRTirtho/spotube/commit/16064f68e882b091401ace4b895e387f46635800))
* **search:** horizontal swipe scroll support for Desktop platform ([d5ff927](https://github.com/KRTirtho/spotube/commit/d5ff927c7273b6e72c5d775ee777f2cbd0d6d05c))
### Bug Fixes
* **artist-page:** SpotubeMarqueeText used in ArtistCard crashes the app ([4279541](https://github.com/KRTirtho/spotube/commit/427954150ab65b250e79fc844fc864abff5b6972))
* **layout:** Fix adaptive UI not working correctly by providing a overriding option ([8c7adde](https://github.com/KRTirtho/spotube/commit/8c7adde890105e0267b71994b7928277f84553e5))
* **local-track:** throwing exception when downloadLocation is empty ([1a3556d](https://github.com/KRTirtho/spotube/commit/1a3556d39e8473cadb6143192c48465dc6485599))
## [2.4.0](https://github.com/KRTirtho/spotube/compare/v2.3.0...v2.4.0) (2022-09-09)
### Features
* Ability to change download location added ([816707c](https://github.com/KRTirtho/spotube/commit/816707c643f8d60d25bc08fd4c8005daa2ba9e63))
* add download multi tracks support for mobile platform ([0476bf7](https://github.com/KRTirtho/spotube/commit/0476bf7ceece034a927d1df6099d8b33036f8a9b))
* add download queue for desktop & initial playlist download support ([08f913e](https://github.com/KRTirtho/spotube/commit/08f913e9761d0f5c447af9dfb6eedb44b675498c))
* add download tab on library ([8d77b69](https://github.com/KRTirtho/spotube/commit/8d77b6900a81aab020e19397e788964b0ac499ff))
* add web support although nothing works just as expected ([2818ed5](https://github.com/KRTirtho/spotube/commit/2818ed5c9dadb9185a52762599c1dd0acd81e6bf))
* **broken:** Broken Warning! Initial Local Audio Player ([c3bf511](https://github.com/KRTirtho/spotube/commit/c3bf5119ebb7c17e8c32f149598508674b0acd39))
* **download:** track table view multi select improvement, tap to play track support, existing track replace confirmation dialog and bulk download confirmation dialog ([e217553](https://github.com/KRTirtho/spotube/commit/e21755322f2cd5f1fba00c5c8cd5c5d1f79e459d))
* **local-tracks:** complete support for local tracks ([e206f16](https://github.com/KRTirtho/spotube/commit/e206f16723ac989ad58006c1b3c90c6691d8cab3))
* **mpris:** MPRIS metadata are now updated in realtime ([d9addcd](https://github.com/KRTirtho/spotube/commit/d9addcda8e9562803bd73016148fab22560ee050))
* **playback:** add repeat track support [#166](https://github.com/KRTirtho/spotube/issues/166) ([cae9993](https://github.com/KRTirtho/spotube/commit/cae99934299bd197c68f626d6c10158d449770b9))
* **synced-lyrics:** animated active text size ([531fae6](https://github.com/KRTirtho/spotube/commit/531fae64f94b21551a7a0da363a9ab0d44f5d3b1))
* **ui:** adaptive TrackTile actions & Setting ListTile ([615d5ce](https://github.com/KRTirtho/spotube/commit/615d5ce901eb0512e84a120b7309c9053238ee36))
### Bug Fixes
* **adaptive-list-tile:** dialog content not updating when content has changed ([a1d4230](https://github.com/KRTirtho/spotube/commit/a1d423090c854ebe319a0fa03fd6e5c4007b1387))
* album & playlist card, player view and album view play button logic ([55852bd](https://github.com/KRTirtho/spotube/commit/55852bd15bc709d61fbba8cbea01ceca791d154c))
* **docs:** indentions ([4a291d5](https://github.com/KRTirtho/spotube/commit/4a291d5f20dabe68f3ed64071624dcbed8327329))
* **downloader:** downloaded track is corrupted for tagging ([2ab1fba](https://github.com/KRTirtho/spotube/commit/2ab1fba3d64147e3c5cf34756dce1cf6046d410a))
* **downloader:** flutter downloader exception on desktop platform and too much width of TrackTile index no. ([d668760](https://github.com/KRTirtho/spotube/commit/d6687603d148ad936530cca4d09e128a59b79b19))
* dropped flutter_downloader deps due to slow download speed and UserDownloads not showing for anonymous ([307a8e2](https://github.com/KRTirtho/spotube/commit/307a8e21df1e39123a1dca4c1b063eab50359581))
* flutter_downloader manifest configuration breaking android support ([f3a0f78](https://github.com/KRTirtho/spotube/commit/f3a0f78fb92ff7ee38b5a9ef9954575d4282f954))
* login screen not using safearea and no dialog bg-color found on light mode in AdaptivePopupMenuButton ([92bc611](https://github.com/KRTirtho/spotube/commit/92bc611c5e901dcabf34086be9287ac20317259a))
* **performance:** always running marquee text causes high GPU usage [#175](https://github.com/KRTirtho/spotube/issues/175) and UserArtist overflow on smaller displays ([a23ce61](https://github.com/KRTirtho/spotube/commit/a23ce614467b4297f495b824f0958ff07c21ae92))
* **playback:** shuffle button sometimes gets stuck and stops working [#183](https://github.com/KRTirtho/spotube/issues/183) ([4240433](https://github.com/KRTirtho/spotube/commit/4240433e3dde6ab948d2674e07e41c27c1f6eac8))
* **player-overlay:** flickering when a track is changed or navigated to another page ([e48b67c](https://github.com/KRTirtho/spotube/commit/e48b67cd47ae54ad9268aead268e444836a67b0d))
* **sidebar:** user image url ([747efc6](https://github.com/KRTirtho/spotube/commit/747efc6ee66bc6c7c917cc02bd134968a0781701))
* **synced-lyrics:** active lyrics contrast ratio ([aba1ba9](https://github.com/KRTirtho/spotube/commit/aba1ba932592923720a36395c057f78820dafecf))
* tabbar overflow in small screen, artist card too small title and synced lyrics contrast increased ([585de8c](https://github.com/KRTirtho/spotube/commit/585de8c1def9750826568317109b242a5e18f28c))
# v2.3.0
### New

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/HoverBuilder.dart';
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class ArtistCard extends StatelessWidget {
final Artist artist;
@ -11,11 +12,12 @@ class ArtistCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final backgroundImage = CachedNetworkImageProvider((artist
.images?.isNotEmpty ??
false)
? artist.images!.first.url!
: "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6");
final backgroundImage = UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
artist.images,
placeholder: ImagePlaceholder.artist,
),
);
return SizedBox(
height: 240,
width: 200,
@ -32,32 +34,55 @@ class ArtistCard extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
blurRadius: 10,
offset: const Offset(0, 3),
spreadRadius: 5,
color: Theme.of(context).shadowColor)
blurRadius: 10,
offset: const Offset(0, 3),
spreadRadius: 5,
color: Theme.of(context).shadowColor,
)
],
),
child: Padding(
padding: const EdgeInsets.all(15),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
CircleAvatar(
maxRadius: 80,
minRadius: 20,
backgroundImage: backgroundImage,
Stack(
children: [
CircleAvatar(
maxRadius: 80,
minRadius: 20,
backgroundImage: backgroundImage,
),
Positioned(
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: const Text(
"Artist",
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
SpotubeMarqueeText(
text: artist.name!,
AutoSizeText(
artist.name!,
maxLines: 2,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.bold,
),
isHovering: isHovering,
),
Text(
"Artist",
style: Theme.of(context).textTheme.subtitle1,
)
],
),
),

View File

@ -1,4 +1,3 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@ -10,6 +9,7 @@ import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerArtistProfile.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TrackTile.dart';
import 'package:spotube/components/Shared/UniversalImage.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/CurrentPlaylist.dart';
@ -78,8 +78,11 @@ class ArtistProfile extends HookConsumerWidget {
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider(
TypeConversionUtils.image_X_UrlString(data.images),
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
data.images,
placeholder: ImagePlaceholder.artist,
),
),
),
Padding(
@ -193,7 +196,9 @@ class ArtistProfile extends HookConsumerWidget {
id: data.id!,
name: "${data.name!} To Tracks",
thumbnail: TypeConversionUtils.image_X_UrlString(
data.images),
data.images,
placeholder: ImagePlaceholder.artist,
),
),
tracks.indexWhere((s) => s.id == currentTrack?.id),
);
@ -233,10 +238,10 @@ class ArtistProfile extends HookConsumerWidget {
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
String? thumbnailUrl =
TypeConversionUtils.image_X_UrlString(
track.value.album?.images,
index:
(track.value.album?.images?.length ?? 1) -
1);
track.value.album?.images,
index: (track.value.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
);
return TrackTile(
playback,
duration: duration,

View File

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

View File

@ -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);
data?.images,
index: (data?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.artist,
);
if (extended.value) {
return Padding(
padding: const EdgeInsets.all(16),
@ -155,7 +182,8 @@ class Sidebar extends HookConsumerWidget {
children: [
CircleAvatar(
backgroundImage:
CachedNetworkImageProvider(avatarImg),
UniversalImage.imageProvider(
avatarImg),
onBackgroundImageError:
(exception, stackTrace) =>
Image.asset(
@ -193,7 +221,7 @@ class Sidebar extends HookConsumerWidget {
onTap: () => goToSettings(context),
child: CircleAvatar(
backgroundImage:
CachedNetworkImageProvider(avatarImg),
UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Image.asset(
"assets/user-placeholder.png",

View File

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

View File

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

View File

@ -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(
(await getTemporaryDirectory()).path,
"spotube",
basenameWithoutExtension(f.path) +
imgMimeToExt[mp3Instance.metaTags["APIC"]?["mime"] ??
"image/jpeg"]!,
))
: null;
if (imageFile != null &&
!await imageFile.exists() &&
mp3Instance.metaTags["APIC"]?["base64"] != null) {
final imageFile = File(join(
(await getTemporaryDirectory()).path,
"spotube",
basenameWithoutExtension(f.path) +
imgMimeToExt[metadata?.picture?.mimeType ?? "image/jpeg"]!,
));
if (!await imageFile.exists() && metadata?.picture != null) {
await imageFile.create(recursive: true);
await imageFile.writeAsBytes(
base64Decode(
mp3Instance.metaTags["APIC"]["base64"],
),
metadata?.picture?.data ?? [],
mode: FileMode.writeOnly,
);
}
Duration duration;
try {
duration = MP3Processor.fromBytes(await bytes).duration;
} catch (e, stack) {
getLogger(MP3Processor).e("[Parsing Mp3]", e, stack);
duration = Duration.zero;
}
final metadata = await tagProcessor.getTagsFromByteArray(bytes);
return {
"metadata": metadata,
"file": f,
"art": imageFile?.path,
"duration": duration,
};
return {"metadata": metadata, "file": f, "art": imageFile.path};
} catch (e, stack) {
getLogger(FutureProvider).e("[Fetching metadata]", e, stack);
return {
"metadata": <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(
playback,
tracks,
currentTrack: track,
);
} else {
playback.stop();
}
}
return playLocalTracks(
playback,
tracks,
currentTrack: track,
);
},
);
},

View File

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

View File

@ -112,6 +112,7 @@ class SyncedLyrics extends HookConsumerWidget {
() => TypeConversionUtils.image_X_UrlString(
playback.track?.album?.images,
index: (playback.track?.album?.images?.length ?? 1) - 1,
placeholder: ImagePlaceholder.albumArt,
),
[playback.track?.album?.images],
);
@ -221,22 +222,23 @@ class SyncedLyrics extends HookConsumerWidget {
: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: AutoSizeText(
lyricSlice.text,
maxLines: 2,
style: Theme.of(context)
.textTheme
.headline4
?.copyWith(
color: isActive
? Colors.white
: palette.bodyTextColor,
// indicating the active state of that lyric slice
fontWeight: isActive
? FontWeight.bold
: null,
),
textAlign: TextAlign.center,
child: AnimatedDefaultTextStyle(
duration: const Duration(
milliseconds: 250),
style: TextStyle(
color: isActive
? Colors.white
: palette.bodyTextColor,
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
fontSize: isActive ? 30 : 26,
),
child: Text(
lyricSlice.text,
maxLines: 2,
textAlign: TextAlign.center,
),
),
),
),

View File

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

View File

@ -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,9 +77,25 @@ class PlayerActions extends HookConsumerWidget {
: null,
),
if (!kIsWeb)
DownloadTrackButton(
track: playback.track,
),
if (isInQueue)
const SizedBox(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
height: 20,
width: 20,
)
else
IconButton(
icon: Icon(
isDownloaded
? Icons.download_done_rounded
: Icons.download_rounded,
),
onPressed: playback.track != null
? () => downloader.addToQueue(playback.track!)
: null,
),
if (auth.isLoggedIn)
FutureBuilder<bool>(
future: playback.track?.id != null

View File

@ -31,25 +31,26 @@ class PlayerControls extends HookConsumerWidget {
child: Column(
children: [
StreamBuilder<Duration>(
stream: playback.player.onPositionChanged,
builder: (context, snapshot) {
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60));
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60));
final currentMinutes = snapshot.hasData
? PrimitiveUtils.zeroPadNumStr(
snapshot.data!.inMinutes.remainder(60))
: "00";
final currentSeconds = snapshot.hasData
? PrimitiveUtils.zeroPadNumStr(
snapshot.data!.inSeconds.remainder(60))
: "00";
stream: playback.player.onPositionChanged,
builder: (context, snapshot) {
final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60));
final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60));
final currentMinutes = snapshot.hasData
? PrimitiveUtils.zeroPadNumStr(
snapshot.data!.inMinutes.remainder(60))
: "00";
final currentSeconds = snapshot.hasData
? PrimitiveUtils.zeroPadNumStr(
snapshot.data!.inSeconds.remainder(60))
: "00";
final sliderMax = duration.inSeconds;
final sliderValue = snapshot.data?.inSeconds ?? 0;
final sliderMax = duration.inSeconds;
final sliderValue = snapshot.data?.inSeconds ?? 0;
return HookBuilder(builder: (context) {
return HookBuilder(
builder: (context) {
final progressStatic =
(sliderMax == 0 || sliderValue > sliderMax)
? 0
@ -84,7 +85,9 @@ class PlayerControls extends HookConsumerWidget {
activeColor: iconColor,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -97,26 +100,25 @@ class PlayerControls extends HookConsumerWidget {
),
],
);
});
}),
},
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.shuffle_rounded),
color: playback.isShuffled
? Theme.of(context).primaryColor
: iconColor,
onPressed: () {
if (playback.track == null || playback.playlist == null) {
return;
}
try {
playback.toggleShuffle();
} catch (e, stack) {
logger.e("onShuffle", e, stack);
}
}),
icon: Icon(
playback.isLoop
? Icons.repeat_one_rounded
: playback.isShuffled
? Icons.shuffle_rounded
: Icons.repeat_rounded,
),
onPressed: playback.track == null || playback.playlist == null
? null
: playback.cyclePlaybackMode,
),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
color: iconColor,
@ -125,7 +127,11 @@ class PlayerControls extends HookConsumerWidget {
}),
IconButton(
icon: playback.status == PlaybackStatus.loading
? const CircularProgressIndicator()
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: Icon(
playback.isPlaying
? Icons.pause_rounded
@ -154,6 +160,7 @@ class PlayerControls extends HookConsumerWidget {
)
],
),
const SizedBox(height: 5)
],
));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -110,7 +111,9 @@ class Search extends HookConsumerWidget {
duration: duration,
thumbnailUrl:
TypeConversionUtils.image_X_UrlString(
track.value.album?.images),
track.value.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async {
var isPlaylistPlaying =
@ -126,6 +129,8 @@ class Search extends HookConsumerWidget {
thumbnail: TypeConversionUtils
.image_X_UrlString(
currentTrack.album?.images,
placeholder:
ImagePlaceholder.albumArt,
),
),
);
@ -143,19 +148,28 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
Scrollbar(
controller: albumController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: Row(
children: albums.map((album) {
return AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(
album,
),
);
}).toList(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: albums.map((album) {
return AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(
album,
),
);
}).toList(),
),
),
),
),
@ -166,21 +180,30 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
Scrollbar(
controller: artistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: Row(
children: artists
.map(
(artist) => Container(
margin: const EdgeInsets.symmetric(
horizontal: 15),
child: ArtistCard(artist),
),
)
.toList(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController,
child: Row(
children: artists
.map(
(artist) => Container(
margin: const EdgeInsets.symmetric(
horizontal: 15),
child: ArtistCard(artist),
),
)
.toList(),
),
),
),
),
@ -191,20 +214,30 @@ class Search extends HookConsumerWidget {
style: Theme.of(context).textTheme.headline5,
),
const SizedBox(height: 10),
Scrollbar(
scrollbarOrientation: breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: Row(
children: playlists
.map(
(playlist) => PlaylistCard(playlist),
)
.toList(),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
child: Row(
children: playlists
.map(
(playlist) => PlaylistCard(playlist),
)
.toList(),
),
),
),
),

View File

@ -24,9 +24,10 @@ class About extends HookWidget {
@override
Widget build(BuildContext context) {
final info = usePackageInfo(
appName: "Spotube",
packageName: "oss.krtirtho.Spotube",
version: "2.3.0");
appName: "Spotube",
packageName: "oss.krtirtho.Spotube",
version: "2.4.1",
);
return ListTile(
leading: Icon(Icons.info_outline_rounded),

View File

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

View File

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

View File

@ -1,228 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:collection/collection.dart';
enum TrackStatus { downloading, idle, done }
class DownloadTrackButton extends HookConsumerWidget {
final Track? track;
const DownloadTrackButton({Key? key, this.track}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
final UserPreferences preferences = ref.watch(userPreferencesProvider);
final Playback playback = ref.watch(playbackProvider);
final status = useState<TrackStatus>(TrackStatus.idle);
YoutubeExplode yt = useMemoized(() => YoutubeExplode());
final outputFile = useState<File?>(null);
String fileName =
"${track?.name} - ${TypeConversionUtils.artists_X_String<Artist>(track?.artists ?? [])}";
useEffect(() {
(() async {
outputFile.value =
File(path.join(preferences.downloadLocation, "$fileName.mp3"));
}());
return null;
}, [fileName, track, preferences.downloadLocation]);
final _downloadTrack = useCallback(() async {
try {
if (track == null || outputFile.value == null) return;
if ((kIsMobile) &&
!await Permission.storage.isGranted &&
!await Permission.storage.isPermanentlyDenied) {
final status = await Permission.storage.request();
if (!status.isGranted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text("Couldn't download track. Not enough permissions"),
),
);
return;
}
}
StreamManifest manifest = await yt.videos.streamsClient
.getManifest((track as SpotubeTrack).ytTrack.url);
File outputLyricsFile = File(
path.join(preferences.downloadLocation, "$fileName-lyrics.txt"));
if (await outputFile.value!.exists()) {
final shouldReplace = await showDialog<bool>(
context: context,
builder: (context) {
return ReplaceDownloadedFileDialog(track: track!);
},
);
if (shouldReplace != true) return;
}
final audioStream = yt.videos.streamsClient
.get(
manifest.audioOnly
.where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(),
)
.asBroadcastStream();
final statusCb = audioStream.listen(
(event) {
if (status.value != TrackStatus.downloading) {
status.value = TrackStatus.downloading;
}
},
onDone: () async {
status.value = TrackStatus.done;
ref.refresh(localTracksProvider);
await Future.delayed(
const Duration(seconds: 3),
() {
if (status.value == TrackStatus.done) {
status.value = TrackStatus.idle;
}
},
);
},
);
if (!await outputFile.value!.exists()) {
await outputFile.value!.create(recursive: true);
}
IOSink outputFileStream = outputFile.value!.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
await outputFileStream.close().then((value) async {
if (status.value == TrackStatus.downloading) {
status.value = TrackStatus.done;
await Future.delayed(
const Duration(seconds: 3),
() {
if (status.value == TrackStatus.done) {
status.value = TrackStatus.idle;
}
},
);
}
return statusCb.cancel();
});
if (preferences.saveTrackLyrics && playback.track != null) {
if (!await outputLyricsFile.exists()) {
await outputLyricsFile.create(recursive: true);
}
final lyrics = await ServiceUtils.getLyrics(
playback.track!.name!,
playback.track!.artists
?.map((s) => s.name)
.whereNotNull()
.toList() ??
[],
apiKey: preferences.geniusAccessToken,
optimizeQuery: true,
);
if (lyrics != null) {
await outputLyricsFile.writeAsString(
"$lyrics\n\nPowered by genius.com",
mode: FileMode.writeOnly,
);
}
}
} on FileSystemException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.red,
content: Text("Download Failed. ${e.message} ${e.path}"),
),
);
}
}, [
track,
status,
yt,
preferences.saveTrackLyrics,
playback.track,
outputFile.value,
preferences.downloadLocation,
fileName
]);
useEffect(() {
return () => yt.close();
}, []);
final outputFileExists = useMemoized(
() => outputFile.value?.existsSync() == true,
[outputFile.value, status.value, track],
);
if (status.value == TrackStatus.downloading) {
return const SizedBox(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
height: 20,
width: 20,
);
} else if (status.value == TrackStatus.done) {
return const Icon(Icons.download_done_rounded);
}
return IconButton(
icon: Icon(
outputFileExists ? Icons.download_done_rounded : Icons.download_rounded,
),
onPressed: track != null &&
track is SpotubeTrack &&
playback.playlist?.isLocal != true
? _downloadTrack
: null,
);
}
}
class ReplaceDownloadedFileDialog extends StatelessWidget {
final Track track;
const ReplaceDownloadedFileDialog({required this.track, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Track ${track.name} Already Exists"),
content:
const Text("Do you want to replace the already downloaded track?"),
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
Navigator.pop(context, true);
},
)
],
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package: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"),
),

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart';
class ReplaceDownloadedFileDialog extends StatelessWidget {
final Track track;
const ReplaceDownloadedFileDialog({required this.track, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text("Track ${track.name} Already Exists"),
content:
const Text("Do you want to replace the already downloaded track?"),
actions: [
TextButton(
child: const Text("No"),
onPressed: () {
Navigator.pop(context, false);
},
),
TextButton(
child: const Text("Yes"),
onPressed: () {
Navigator.pop(context, true);
},
)
],
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
import 'package:spotube/components/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(

View File

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

View File

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

View File

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

View File

@ -1,157 +0,0 @@
import 'package:dart_tags/dart_tags.dart';
class Id3Tags {
Id3Tags({
this.tsse,
this.title,
this.album,
this.tpe2,
this.comment,
this.tcop,
this.tdrc,
this.genre,
this.picture,
});
String? tsse;
String? title;
String? album;
String? tpe2;
Comment? comment;
String? tcop;
String? tdrc;
String? genre;
AttachedPicture? picture;
factory Id3Tags.fromJson(Map<String, dynamic> json) => Id3Tags(
tsse: json["TSSE"],
title: json["title"],
album: json["album"],
tpe2: json["TPE2"],
comment: json["comment"]?["eng:"] is Comment
? json["comment"]["eng:"]
: CommentJson.fromJson(Map.from(
json["comment"]?["eng:"] ?? {},
)),
tcop: json["TCOP"],
tdrc: json["TDRC"],
genre: json["genre"],
picture: json["picture"]?["Cover (front)"] is AttachedPicture
? json["picture"]["Cover (front)"]
: AttachedPictureJson.fromJson(Map.from(
json["picture"]?["Cover (front)"] ?? {},
)),
);
factory Id3Tags.fromId3v1Tags(Id3v1Tags v1tags) => Id3Tags(
album: v1tags.album,
comment: Comment("", "", v1tags.comment ?? ""),
genre: v1tags.genre,
title: v1tags.title,
tcop: v1tags.year,
tdrc: v1tags.year,
tpe2: v1tags.artist,
);
Map<String, dynamic> toJson() => {
"TSSE": tsse,
"title": title,
"album": album,
"TPE2": tpe2,
"comment": comment,
"TCOP": tcop,
"TDRC": tdrc,
"genre": genre,
"picture": picture,
};
String? get artist => tpe2;
String? get year => tdrc;
Map<String, String> toAndroidJson(String artwork) {
return {
"title": title ?? "Unknown",
"artist": artist ?? "Unknown",
"album": album ?? "Unknown",
"genre": genre ?? "Unknown",
"artwork": artwork,
"year": year ?? "Unknown",
};
}
}
extension CommentJson on Comment {
static fromJson(Map<String, dynamic> json) => Comment(
json["lang"] ?? "",
json["description"] ?? "",
json["comment"] ?? "",
);
Map<String, dynamic> toJson() => {
"comment": comment,
"description": description,
"key": key,
"lang": lang,
};
}
extension AttachedPictureJson on AttachedPicture {
static fromJson(Map<String, dynamic> json) => AttachedPicture(
json["mime"] ?? "",
json["imageTypeCode"] ?? 0,
json["description"] ?? "",
List<int>.from(json["imageData"] ?? []),
);
Map<String, dynamic> toJson() => {
"description": description,
"imageData": imageData,
"imageData64": imageData64,
"imageType": imageType,
"imageTypeCode": imageTypeCode,
"key": key,
"mime": mime,
};
}
class Id3v1Tags {
String? title;
String? artist;
String? album;
String? year;
String? comment;
String? track;
String? genre;
Id3v1Tags({
this.title,
this.artist,
this.album,
this.year,
this.comment,
this.track,
this.genre,
});
Id3v1Tags.fromJson(Map<String, dynamic> json) {
title = json['title'];
artist = json['artist'];
album = json['album'];
year = json['year'];
comment = json['comment'];
track = json['track'];
genre = json['genre'];
}
Map<String, dynamic> toJson() {
return {
'title': title,
'artist': artist,
'album': album,
'year': year,
'comment': comment,
'track': track,
'genre': genre,
};
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package: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);

View File

@ -1,22 +1,18 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dart_tags/dart_tags.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/widgets.dart' hide Image;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:queue/queue.dart';
import 'package:path/path.dart' as path;
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Library/UserLocalTracks.dart';
import 'package:spotube/models/Id3Tags.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/models/Logger.dart';
import 'package:spotube/models/SpotubeTrack.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
import 'package:spotube/provider/YouTube.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
@ -63,7 +59,7 @@ class Downloader with ChangeNotifier {
RegExp(r'[/\\?%*:|"<>]'),
"",
);
final filename = '$cleanTitle.mp3';
final filename = '$cleanTitle.m4a';
final file = File(path.join(downloadPath, filename));
try {
logger.v("[addToQueue] Download starting for ${file.path}");
@ -97,54 +93,38 @@ class Downloader with ChangeNotifier {
"[addToQueue] Download of ${file.path} is done successfully",
);
// Tagging isn't supported in Android currently
if (kIsAndroid) return;
logger.v(
"[addToQueue] Writing metadata to ${file.path}",
);
final imageUri = TypeConversionUtils.image_X_UrlString(
track.album?.images ?? [],
placeholder: ImagePlaceholder.online,
);
final response = await get(
Uri.parse(
imageUri,
),
);
final picture = AttachedPicture.base64(
response.headers["Content-Type"] ?? "image/jpeg",
3,
track.name!,
base64Encode(response.bodyBytes),
);
// write id3 metadata
final tag = Id3Tags(
album: track.album?.name,
picture: picture,
title: track.name,
genre: "Spotube",
tcop: track.ytTrack.uploadDate?.year.toString(),
tdrc: track.ytTrack.uploadDate?.year.toString(),
tpe2: TypeConversionUtils.artists_X_String<Artist>(
track.artists ?? [],
),
tsse: "",
comment: Comment(
"eng",
track.ytTrack.description,
track.ytTrack.title,
),
);
final response = await get(Uri.parse(imageUri));
logger.v("[addToQueue] Writing metadata to ${file.path}");
final taggedMp3 = await tagProcessor.putTagsToByteArray(
file.readAsBytes(),
[
Tag()
..type = "ID3"
..version = "2.4.0"
..tags = tag.toJson()
],
await MetadataGod.writeMetadata(
file,
Metadata(
title: track.name,
artist: track.artists?.map((a) => a.name).join(", "),
album: track.album?.name,
albumArtist: track.artists?.map((a) => a.name).join(", "),
year: track.album?.releaseDate != null
? int.tryParse(track.album!.releaseDate!)
: null,
trackNumber: track.trackNumber,
discNumber: track.discNumber,
durationMs: track.durationMs?.toDouble(),
fileSize: file.lengthSync(),
trackTotal: track.album?.tracks?.length,
picture: response.headers['content-type'] != null
? Image(
data: response.bodyBytes,
mimeType: response.headers['content-type']!,
)
: null,
),
);
await file.writeAsBytes(taggedMp3);
logger.v(
"[addToQueue] Writing metadata to ${file.path} is successful",
);

View File

@ -37,9 +37,15 @@ enum AudioQuality {
low,
}
enum PlaybackMode {
repeat,
shuffle,
normal,
}
class Playback extends PersistedChangeNotifier {
// player properties
bool isShuffled;
PlaybackMode playbackMode;
bool isPlaying;
Duration currentDuration;
double volume;
@ -72,9 +78,9 @@ class Playback extends PersistedChangeNotifier {
required this.youtube,
required this.ref,
this.mobileAudioService,
}) : volume = 0,
isShuffled = false,
}) : volume = 1,
isPlaying = false,
playbackMode = PlaybackMode.normal,
currentDuration = Duration.zero,
_subscriptions = [],
status = PlaybackStatus.idle,
@ -93,6 +99,10 @@ class Playback extends PersistedChangeNotifier {
await player.setVolume(volume);
}
addListener(() {
_linuxAudioService?.player.updateProperties(this);
});
_subscriptions.addAll([
player.onPlayerStateChanged.listen(
(state) async {
@ -102,7 +112,13 @@ class Playback extends PersistedChangeNotifier {
),
player.onPlayerComplete.listen((_) {
if (track?.id != null) {
seekForward();
if (isLoop) {
final prevTrack = track;
track = null;
play(prevTrack!);
} else if (playlist != null) {
seekForward();
}
} else {
isPlaying = false;
status = PlaybackStatus.idle;
@ -210,6 +226,7 @@ class Playback extends PersistedChangeNotifier {
artUri: Uri.parse(
TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.online,
),
),
duration: track.ytTrack.duration,
@ -249,12 +266,26 @@ class Playback extends PersistedChangeNotifier {
isPlaying ? await pause() : await resume();
}
toggleShuffle() {
final result = isShuffled ? playlist?.unshuffle() : playlist?.shuffle();
if (result == true) {
isShuffled = !isShuffled;
notifyListeners();
void cyclePlaybackMode() {
switch (playbackMode) {
case PlaybackMode.normal:
playbackMode = PlaybackMode.shuffle;
playlist?.shuffle();
break;
case PlaybackMode.shuffle:
playbackMode = PlaybackMode.repeat;
playlist?.unshuffle();
break;
case PlaybackMode.repeat:
playbackMode = PlaybackMode.normal;
break;
}
notifyListeners();
}
void setPlaybackMode(PlaybackMode mode) {
playbackMode = mode;
notifyListeners();
}
Future<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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
bitsdojo_window_linux
metadata_god
url_launcher_linux
)

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
bitsdojo_window_windows
metadata_god
permission_handler_windows
url_launcher_windows
)