mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
Merge branch 'master' into build
This commit is contained in:
commit
56d5f68e8c
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,2 +1,5 @@
|
|||||||
open_collective: spotube
|
open_collective: spotube
|
||||||
ko_fi: krtirtho
|
ko_fi: krtirtho
|
||||||
|
patreon: krtirtho
|
||||||
|
custom:
|
||||||
|
- "https://www.buymeacoffee.com/krtirtho"
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,3 +1,14 @@
|
|||||||
|
# v2.2.1
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Page transitions defaulted to material you design
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- Mini Player flickering on random state updates
|
||||||
|
- Track More Options not showing when not logged in
|
||||||
|
- Wrong link to Client ID & Client Secret tutorial in Login page
|
||||||
|
- Changing preferences in Settings resets the entire Playback
|
||||||
|
|
||||||
# v2.2.0
|
# v2.2.0
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
25
README.md
25
README.md
@ -44,7 +44,13 @@ Following are the features that currently spotube offers:
|
|||||||
- Synced Lyrics
|
- Synced Lyrics
|
||||||
- Downloadable track
|
- Downloadable track
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/spotube?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-spotube" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=327965&theme=dark" alt="Spotube - A lightweight+free Spotify crossplatform-client made with flutter | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
# Support this project
|
||||||
|
|
||||||
|
<a href="https://patreon.com/krtirtho"><img src="https://img.shields.io/endpoint.svg?url=https://moshef9.wixsite.com/patreon-badge/_functions/badge/?username=krtirtho" alt="Patreon donate button" /> </a>
|
||||||
|
|
||||||
|
[](https://opencollective.com/spotube)
|
||||||
|
[](https://www.buymeacoffee.com/krtirtho)
|
||||||
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@ -84,23 +90,12 @@ Get the latest nightly builds of Spotube [here](https://nightly.link/KRTirtho/sp
|
|||||||
<img width='480' alt='step 2' src='https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png'/>
|
<img width='480' alt='step 2' src='https://user-images.githubusercontent.com/61944859/111762507-473f4700-88cb-11eb-91f3-d480e9584883.png'/>
|
||||||
|
|
||||||
- **MOST IMPORTANT:** Give the app a name & description. Then Edit settings & add `http://localhost:4304/auth/spotify/callback` as **Redirect URI** for the app. Its important for authenticating<br/>
|
- **MOST IMPORTANT:** Give the app a name & description. Then Edit settings & add `http://localhost:4304/auth/spotify/callback` as **Redirect URI** for the app. Its important for authenticating<br/>
|
||||||
<img width='720' alt='setp-3' src='https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png'/>
|
<img width='720' alt='step-3-a' src='https://user-images.githubusercontent.com/61944859/172991668-fa40f247-1118-4aba-a749-e669b732fa4d.jpg' />
|
||||||
|
<img width='720' alt='setp-3-b' src='https://user-images.githubusercontent.com/61944859/111768971-d308a180-88d2-11eb-9108-3e7444cef049.png'/>
|
||||||
|
|
||||||
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields<br/>
|
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields<br/>
|
||||||
<img width='480' alt='step-4' src='https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png'/>
|
<img width='480' alt='step-4' src='https://user-images.githubusercontent.com/61944859/111769501-7fe31e80-88d3-11eb-8fc1-f3655dbd4711.png'/>
|
||||||
|
|
||||||
### Setup <b>Genius Lyrics</b>
|
|
||||||
|
|
||||||
- Signup/Login into [genius](https://genius.com/signup) for **lyrics**
|
|
||||||
- Go To [Genius Developer Portal](https://genius.com/api-clients/new) for creating an API client<br/>
|
|
||||||
<img width='480' alt='Step 2' src='https://user-images.githubusercontent.com/61944859/158823216-b4942731-c4c5-46c8-8b60-82a372b51cc5.png' />
|
|
||||||
- Generate & copy access token<br/>
|
|
||||||
<img width='480' alt='Step 3' src='https://user-images.githubusercontent.com/61944859/158822817-f04da060-3094-4a3b-8ace-a936d0cda8db.png' />
|
|
||||||
- Paste the copied access token in Spotube's Settings<br/>
|
|
||||||
<img width='480' alt='Step 4' src='https://user-images.githubusercontent.com/61944859/158823984-17f08534-5c92-41bc-918a-23194aad00f5.png' />
|
|
||||||
|
|
||||||
> **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
|
|
||||||
|
|
||||||
# TODO:
|
# TODO:
|
||||||
|
|
||||||
- [x] Compile, Debug & Build for **MacOS**
|
- [x] Compile, Debug & Build for **MacOS**
|
||||||
@ -151,6 +146,8 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour
|
|||||||
- [marquee](https://github.com/MarcelGarus/marquee) - ⏩ A Flutter widget that scrolls text infinitely. Provides many customizations including custom scroll directions, durations, curves as well as pauses after every round
|
- [marquee](https://github.com/MarcelGarus/marquee) - ⏩ A Flutter widget that scrolls text infinitely. Provides many customizations including custom scroll directions, durations, curves as well as pauses after every round
|
||||||
- [scroll_to_index](https://github.com/quire-io/scroll-to-index) - scroll to index with fixed/variable row height inside Flutter scrollable widget
|
- [scroll_to_index](https://github.com/quire-io/scroll-to-index) - scroll to index with fixed/variable row height inside Flutter scrollable widget
|
||||||
- [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/) - This Flutter plugin provides an API for querying information about an application package.
|
- [package_info_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/) - This Flutter plugin provides an API for querying information about an application package.
|
||||||
|
- [version](https://github.com/dartninja/version) - A dart library providing a Version class
|
||||||
|
- [audio_service](https://github.com/ryanheise/audio_service) - Flutter plugin to play audio in the background while the screen is off.
|
||||||
|
|
||||||
|
|
||||||
# Social handlers
|
# Social handlers
|
||||||
|
BIN
assets/empty_box.png
Normal file
BIN
assets/empty_box.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
@ -40,7 +41,6 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
Playback playback = ref.watch(playbackProvider);
|
Playback playback = ref.watch(playbackProvider);
|
||||||
|
|
||||||
final isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
|
||||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||||
final Auth auth = ref.watch(authProvider);
|
final Auth auth = ref.watch(authProvider);
|
||||||
|
|
||||||
@ -48,85 +48,60 @@ class AlbumView extends HookConsumerWidget {
|
|||||||
final albumSavedSnapshot =
|
final albumSavedSnapshot =
|
||||||
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
ref.watch(albumIsSavedForCurrentUserQuery(album.id!));
|
||||||
|
|
||||||
return SafeArea(
|
final albumArt =
|
||||||
child: Scaffold(
|
useMemoized(() => imageToUrlString(album.images), [album.images]);
|
||||||
body: Column(
|
|
||||||
children: [
|
return TrackCollectionView(
|
||||||
PageWindowTitleBar(
|
id: album.id!,
|
||||||
leading: Row(
|
isPlaying: playback.currentPlaylist?.id != null &&
|
||||||
children: [
|
playback.currentPlaylist?.id == album.id,
|
||||||
// nav back
|
title: album.name!,
|
||||||
const BackButton(),
|
titleImage: albumArt,
|
||||||
// heart playlist
|
tracksSnapshot: tracksSnapshot,
|
||||||
if (auth.isLoggedIn)
|
album: album,
|
||||||
albumSavedSnapshot.when(
|
onPlay: ([track]) {
|
||||||
data: (isSaved) {
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
return HeartButton(
|
playPlaylist(
|
||||||
isLiked: isSaved,
|
playback,
|
||||||
onPressed: () {
|
tracksSnapshot.asData!.value
|
||||||
(isSaved
|
.map((track) => simpleTrackToTrack(track, album))
|
||||||
? spotify.me.removeAlbums(
|
.toList(),
|
||||||
[album.id!],
|
currentTrack: track,
|
||||||
)
|
);
|
||||||
: spotify.me.saveAlbums(
|
}
|
||||||
[album.id!],
|
},
|
||||||
))
|
onShare: () {
|
||||||
.whenComplete(() {
|
Clipboard.setData(
|
||||||
ref.refresh(
|
ClipboardData(text: "https://open.spotify.com/album/${album.id}"),
|
||||||
albumIsSavedForCurrentUserQuery(
|
);
|
||||||
album.id!,
|
},
|
||||||
),
|
heartBtn: auth.isLoggedIn
|
||||||
);
|
? albumSavedSnapshot.when(
|
||||||
ref.refresh(currentUserAlbumsQuery);
|
data: (isSaved) {
|
||||||
});
|
return HeartButton(
|
||||||
},
|
isLiked: isSaved,
|
||||||
);
|
onPressed: () {
|
||||||
},
|
(isSaved
|
||||||
error: (error, _) => Text("Error $error"),
|
? spotify.me.removeAlbums(
|
||||||
loading: () => const CircularProgressIndicator()),
|
[album.id!],
|
||||||
// play playlist
|
)
|
||||||
IconButton(
|
: spotify.me.saveAlbums(
|
||||||
icon: Icon(
|
[album.id!],
|
||||||
isPlaylistPlaying
|
))
|
||||||
? Icons.stop_rounded
|
.whenComplete(() {
|
||||||
: Icons.play_arrow_rounded,
|
ref.refresh(
|
||||||
),
|
albumIsSavedForCurrentUserQuery(
|
||||||
onPressed: tracksSnapshot.asData?.value != null
|
album.id!,
|
||||||
? () => playPlaylist(
|
),
|
||||||
playback,
|
);
|
||||||
tracksSnapshot.asData!.value.map((trackSmp) {
|
ref.refresh(currentUserAlbumsQuery);
|
||||||
return simpleTrackToTrack(trackSmp, album);
|
});
|
||||||
}).toList(),
|
},
|
||||||
)
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Text(album.name!,
|
|
||||||
style: Theme.of(context).textTheme.headline4),
|
|
||||||
),
|
|
||||||
tracksSnapshot.when(
|
|
||||||
data: (data) {
|
|
||||||
List<Track> tracks = data.map((trackSmp) {
|
|
||||||
return simpleTrackToTrack(trackSmp, album);
|
|
||||||
}).toList();
|
|
||||||
return TracksTableView(
|
|
||||||
tracks,
|
|
||||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
|
||||||
playback,
|
|
||||||
tracks,
|
|
||||||
currentTrack: currentTrack,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, _) => Text("Error $error"),
|
error: (error, _) => Text("Error $error"),
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const CircularProgressIndicator())
|
||||||
),
|
: null,
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:marquee/marquee.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
import 'package:spotube/components/Shared/SpotubeMarqueeText.dart';
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
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/PageWindowTitleBar.dart';
|
||||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
@ -14,7 +15,6 @@ import 'package:spotube/helpers/readable-number.dart';
|
|||||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
@ -49,7 +49,6 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
final update = useForceUpdate();
|
|
||||||
|
|
||||||
final Playback playback = ref.watch(playbackProvider);
|
final Playback playback = ref.watch(playbackProvider);
|
||||||
|
|
||||||
@ -66,268 +65,264 @@ class ArtistProfile extends HookConsumerWidget {
|
|||||||
leading: BackButton(),
|
leading: BackButton(),
|
||||||
),
|
),
|
||||||
body: artistsSnapshot.when<Widget>(
|
body: artistsSnapshot.when<Widget>(
|
||||||
data: (data) {
|
data: (data) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: parentScrollController,
|
controller: parentScrollController,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
runAlignment: WrapAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 50),
|
const SizedBox(width: 50),
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: avatarWidth,
|
radius: avatarWidth,
|
||||||
backgroundImage: CachedNetworkImageProvider(
|
backgroundImage: CachedNetworkImageProvider(
|
||||||
imageToUrlString(data.images),
|
imageToUrlString(data.images),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.all(20),
|
Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.all(20),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(
|
Container(
|
||||||
horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: 10, vertical: 5),
|
||||||
color: Colors.blue,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(50)),
|
color: Colors.blue,
|
||||||
child: Text(data.type!.toUpperCase(),
|
borderRadius: BorderRadius.circular(50)),
|
||||||
style: chipTextVariant?.copyWith(
|
child: Text(data.type!.toUpperCase(),
|
||||||
color: Colors.white)),
|
style: chipTextVariant?.copyWith(
|
||||||
),
|
color: Colors.white)),
|
||||||
Text(
|
|
||||||
data.name!,
|
|
||||||
style: breakpoint.isSm
|
|
||||||
? textTheme.headline4
|
|
||||||
: textTheme.headline2,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"${toReadableNumber(data.followers!.total!.toDouble())} followers",
|
|
||||||
style: breakpoint.isSm
|
|
||||||
? textTheme.bodyText1
|
|
||||||
: textTheme.headline5,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
isFollowingSnapshot.when(
|
|
||||||
data: (isFollowing) {
|
|
||||||
return OutlinedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
try {
|
|
||||||
isFollowing
|
|
||||||
? await spotify.me.unfollow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
)
|
|
||||||
: await spotify.me.follow(
|
|
||||||
FollowingType.artist,
|
|
||||||
[artistId],
|
|
||||||
);
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e(
|
|
||||||
"FollowButton.onPressed",
|
|
||||||
e,
|
|
||||||
stack,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
ref.refresh(
|
|
||||||
currentUserFollowsArtistQuery(
|
|
||||||
artistId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
isFollowing
|
|
||||||
? "Following"
|
|
||||||
: "Follow",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, stackTrace) => Container(),
|
|
||||||
loading: () =>
|
|
||||||
const CircularProgressIndicator
|
|
||||||
.adaptive()),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.share_rounded),
|
|
||||||
onPressed: () {
|
|
||||||
Clipboard.setData(
|
|
||||||
ClipboardData(
|
|
||||||
text: data.externalUrls?.spotify),
|
|
||||||
).then((val) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
width: 300,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
content: Text(
|
|
||||||
"Artist URL copied to clipboard",
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 50),
|
|
||||||
topTracksSnapshot.when(
|
|
||||||
data: (topTracks) {
|
|
||||||
final isPlaylistPlaying =
|
|
||||||
playback.currentPlaylist?.id == data.id;
|
|
||||||
playPlaylist(List<Track> tracks,
|
|
||||||
{Track? currentTrack}) async {
|
|
||||||
currentTrack ??= tracks.first;
|
|
||||||
if (!isPlaylistPlaying) {
|
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
|
||||||
tracks: tracks,
|
|
||||||
id: data.id!,
|
|
||||||
name: "${data.name!} To Tracks",
|
|
||||||
thumbnail: imageToUrlString(data.images),
|
|
||||||
);
|
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
} else if (isPlaylistPlaying &&
|
|
||||||
currentTrack.id != null &&
|
|
||||||
currentTrack.id != playback.currentTrack?.id) {
|
|
||||||
playback.setCurrentTrack = currentTrack;
|
|
||||||
}
|
|
||||||
await playback.startPlaying();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Top Tracks",
|
|
||||||
style: Theme.of(context).textTheme.headline4,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 5),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).primaryColor,
|
|
||||||
borderRadius: BorderRadius.circular(50),
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(isPlaylistPlaying
|
|
||||||
? Icons.stop_rounded
|
|
||||||
: Icons.play_arrow_rounded),
|
|
||||||
color: Colors.white,
|
|
||||||
onPressed: () =>
|
|
||||||
playPlaylist(topTracks.toList()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
...topTracks.toList().asMap().entries.map((track) {
|
|
||||||
String duration =
|
|
||||||
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
|
||||||
String? thumbnailUrl = imageToUrlString(
|
|
||||||
track.value.album?.images,
|
|
||||||
index:
|
|
||||||
(track.value.album?.images?.length ?? 1) -
|
|
||||||
1);
|
|
||||||
return TrackTile(
|
|
||||||
playback,
|
|
||||||
duration: duration,
|
|
||||||
track: track,
|
|
||||||
thumbnailUrl: thumbnailUrl,
|
|
||||||
onTrackPlayButtonPressed: (currentTrack) =>
|
|
||||||
playPlaylist(
|
|
||||||
topTracks.toList(),
|
|
||||||
currentTrack: track.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
error: (error, stack) =>
|
|
||||||
Text("Failed to find top tracks $error"),
|
|
||||||
loading: () => const Center(
|
|
||||||
child: CircularProgressIndicator.adaptive()),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 50),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Albums",
|
|
||||||
style: Theme.of(context).textTheme.headline4,
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: const Text("See All"),
|
|
||||||
onPressed: () {
|
|
||||||
GoRouter.of(context).push(
|
|
||||||
"/artist-album/$artistId",
|
|
||||||
extra: data.name ?? "KRTX",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
albums.when(
|
|
||||||
data: (albums) {
|
|
||||||
return Scrollbar(
|
|
||||||
controller: scrollController,
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: albums.items
|
|
||||||
?.map((album) => AlbumCard(album))
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
data.name!,
|
||||||
|
style: breakpoint.isSm
|
||||||
|
? textTheme.headline4
|
||||||
|
: textTheme.headline2,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${toReadableNumber(data.followers!.total!.toDouble())} followers",
|
||||||
|
style: breakpoint.isSm
|
||||||
|
? textTheme.bodyText1
|
||||||
|
: textTheme.headline5,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
isFollowingSnapshot.when(
|
||||||
|
data: (isFollowing) {
|
||||||
|
return OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
isFollowing
|
||||||
|
? await spotify.me.unfollow(
|
||||||
|
FollowingType.artist,
|
||||||
|
[artistId],
|
||||||
|
)
|
||||||
|
: await spotify.me.follow(
|
||||||
|
FollowingType.artist,
|
||||||
|
[artistId],
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e(
|
||||||
|
"FollowButton.onPressed",
|
||||||
|
e,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
ref.refresh(
|
||||||
|
currentUserFollowsArtistQuery(
|
||||||
|
artistId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
isFollowing ? "Following" : "Follow",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, stackTrace) => Container(),
|
||||||
|
loading: () =>
|
||||||
|
const CircularProgressIndicator
|
||||||
|
.adaptive()),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.share_rounded),
|
||||||
|
onPressed: () {
|
||||||
|
Clipboard.setData(
|
||||||
|
ClipboardData(
|
||||||
|
text: data.externalUrls?.spotify),
|
||||||
|
).then((val) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
width: 300,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
content: Text(
|
||||||
|
"Artist URL copied to clipboard",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
topTracksSnapshot.when(
|
||||||
|
data: (topTracks) {
|
||||||
|
final isPlaylistPlaying =
|
||||||
|
playback.currentPlaylist?.id == data.id;
|
||||||
|
playPlaylist(List<Track> tracks,
|
||||||
|
{Track? currentTrack}) async {
|
||||||
|
currentTrack ??= tracks.first;
|
||||||
|
if (!isPlaylistPlaying) {
|
||||||
|
playback.setCurrentPlaylist = CurrentPlaylist(
|
||||||
|
tracks: tracks,
|
||||||
|
id: data.id!,
|
||||||
|
name: "${data.name!} To Tracks",
|
||||||
|
thumbnail: imageToUrlString(data.images),
|
||||||
|
);
|
||||||
|
playback.setCurrentTrack = currentTrack;
|
||||||
|
} else if (isPlaylistPlaying &&
|
||||||
|
currentTrack.id != null &&
|
||||||
|
currentTrack.id != playback.currentTrack?.id) {
|
||||||
|
playback.setCurrentTrack = currentTrack;
|
||||||
|
}
|
||||||
|
await playback.startPlaying();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Top Tracks",
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(isPlaylistPlaying
|
||||||
|
? Icons.stop_rounded
|
||||||
|
: Icons.play_arrow_rounded),
|
||||||
|
color: Colors.white,
|
||||||
|
onPressed: () =>
|
||||||
|
playPlaylist(topTracks.toList()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...topTracks.toList().asMap().entries.map((track) {
|
||||||
|
String duration =
|
||||||
|
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
|
String? thumbnailUrl = imageToUrlString(
|
||||||
|
track.value.album?.images,
|
||||||
|
index:
|
||||||
|
(track.value.album?.images?.length ?? 1) - 1);
|
||||||
|
return TrackTile(
|
||||||
|
playback,
|
||||||
|
duration: duration,
|
||||||
|
track: track,
|
||||||
|
thumbnailUrl: thumbnailUrl,
|
||||||
|
onTrackPlayButtonPressed: (currentTrack) =>
|
||||||
|
playPlaylist(
|
||||||
|
topTracks.toList(),
|
||||||
|
currentTrack: track.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
error: (error, stack) =>
|
||||||
|
Text("Failed to find top tracks $error"),
|
||||||
|
loading: () => const Center(
|
||||||
|
child: CircularProgressIndicator.adaptive()),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 50),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Albums",
|
||||||
|
style: Theme.of(context).textTheme.headline4,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text("See All"),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context).push(
|
||||||
|
"/artist-album/$artistId",
|
||||||
|
extra: data.name ?? "KRTX",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
albums.when(
|
||||||
|
data: (albums) {
|
||||||
|
return Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: albums.items
|
||||||
|
?.map((album) => AlbumCard(album))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
error: (error, stackTrack) =>
|
},
|
||||||
Text("Failed to get Artist albums $error"),
|
error: (error, stackTrack) =>
|
||||||
loading: () => const CircularProgressIndicator.adaptive(),
|
Text("Failed to get Artist albums $error"),
|
||||||
),
|
loading: () => const CircularProgressIndicator.adaptive(),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
"Fans also likes",
|
Text(
|
||||||
style: Theme.of(context).textTheme.headline4,
|
"Fans also likes",
|
||||||
),
|
style: Theme.of(context).textTheme.headline4,
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
relatedArtists.when(
|
const SizedBox(height: 10),
|
||||||
data: (artists) {
|
relatedArtists.when(
|
||||||
return Center(
|
data: (artists) {
|
||||||
child: Wrap(
|
return Center(
|
||||||
spacing: 20,
|
child: Wrap(
|
||||||
runSpacing: 20,
|
spacing: 20,
|
||||||
children: artists
|
runSpacing: 20,
|
||||||
.map((artist) => ArtistCard(artist))
|
children: artists
|
||||||
.toList(),
|
.map((artist) => ArtistCard(artist))
|
||||||
),
|
.toList(),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
error: (error, stackTrack) =>
|
},
|
||||||
Text("Failed to get Artist albums $error"),
|
error: (error, stackTrack) =>
|
||||||
loading: () => const CircularProgressIndicator.adaptive(),
|
Text("Failed to get Artist albums $error"),
|
||||||
),
|
loading: () => const CircularProgressIndicator.adaptive(),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
error: (_, __) => const Text("Life's miserable"),
|
},
|
||||||
loading: () =>
|
error: (_, __) => const Text("Life's miserable"),
|
||||||
const Center(child: CircularProgressIndicator.adaptive())),
|
loading: () => const ShimmerArtistProfile(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||||
|
import 'package:spotube/components/Shared/NotFound.dart';
|
||||||
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
import 'package:spotube/hooks/usePaginatedFutureProvider.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
@ -71,6 +73,15 @@ class CategoryCard extends HookConsumerWidget {
|
|||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
|
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
|
||||||
|
noItemsFoundIndicatorBuilder: (context) {
|
||||||
|
return const NotFound();
|
||||||
|
},
|
||||||
|
firstPageProgressIndicatorBuilder: (context) {
|
||||||
|
return const ShimmerPlaybuttonCard();
|
||||||
|
},
|
||||||
|
newPageProgressIndicatorBuilder: (context) {
|
||||||
|
return const ShimmerPlaybuttonCard();
|
||||||
|
},
|
||||||
itemBuilder: (context, playlist, index) {
|
itemBuilder: (context, playlist, index) {
|
||||||
return PlaylistCard(playlist);
|
return PlaylistCard(playlist);
|
||||||
},
|
},
|
||||||
|
@ -11,6 +11,7 @@ import 'package:spotify/spotify.dart' hide Image, Player, Search;
|
|||||||
import 'package:spotube/components/Category/CategoryCard.dart';
|
import 'package:spotube/components/Category/CategoryCard.dart';
|
||||||
import 'package:spotube/components/Home/Sidebar.dart';
|
import 'package:spotube/components/Home/Sidebar.dart';
|
||||||
import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
|
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/Lyrics/SyncedLyrics.dart';
|
||||||
import 'package:spotube/components/Search/Search.dart';
|
import 'package:spotube/components/Search/Search.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||||
@ -138,6 +139,10 @@ class Home extends HookConsumerWidget {
|
|||||||
pagingController: pagingController,
|
pagingController: pagingController,
|
||||||
builderDelegate:
|
builderDelegate:
|
||||||
PagedChildBuilderDelegate<Category>(
|
PagedChildBuilderDelegate<Category>(
|
||||||
|
firstPageProgressIndicatorBuilder: (_) =>
|
||||||
|
const ShimmerCategories(),
|
||||||
|
newPageProgressIndicatorBuilder: (_) =>
|
||||||
|
const ShimmerCategories(),
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return CategoryCard(item);
|
return CategoryCard(item);
|
||||||
},
|
},
|
||||||
|
@ -3,11 +3,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Image;
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
|
||||||
class Sidebar extends HookConsumerWidget {
|
class Sidebar extends HookConsumerWidget {
|
||||||
final int selectedIndex;
|
final int selectedIndex;
|
||||||
@ -36,7 +35,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
final breakpoints = useBreakpoints();
|
final breakpoints = useBreakpoints();
|
||||||
if (breakpoints.isSm) return Container();
|
if (breakpoints.isSm) return Container();
|
||||||
final extended = useState(false);
|
final extended = useState(false);
|
||||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (breakpoints.isMd && extended.value) {
|
if (breakpoints.isMd && extended.value) {
|
||||||
@ -78,11 +77,10 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
: _buildSmallLogo(),
|
: _buildSmallLogo(),
|
||||||
trailing: FutureBuilder<User>(
|
trailing: meSnapshot.when(
|
||||||
future: spotify.me.get(),
|
data: (data) {
|
||||||
builder: (context, snapshot) {
|
final avatarImg = imageToUrlString(data.images,
|
||||||
final avatarImg = imageToUrlString(snapshot.data?.images,
|
index: (data.images?.length ?? 1) - 1);
|
||||||
index: (snapshot.data?.images?.length ?? 1) - 1);
|
|
||||||
return extended.value
|
return extended.value
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@ -97,7 +95,7 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data?.displayName ?? "Guest",
|
data.displayName ?? "Guest",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@ -116,6 +114,8 @@ class Sidebar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
error: (e, _) => Text("Error $e"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||||
|
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||||
import 'package:spotube/helpers/simple-album-to-album.dart';
|
import 'package:spotube/helpers/simple-album-to-album.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ class UserAlbums extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator.adaptive()),
|
loading: () => const Center(child: ShimmerPlaybuttonCard(count: 7)),
|
||||||
error: (_, __) => const Text("Failure is the pillar of success"),
|
error: (_, __) => const Text("Failure is the pillar of success"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart' hide Image;
|
import 'package:flutter/material.dart' hide Image;
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||||
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
import 'package:spotube/components/Playlist/PlaylistCreateDialog.dart';
|
||||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||||
@ -13,8 +14,7 @@ class UserPlaylists extends ConsumerWidget {
|
|||||||
final playlists = ref.watch(currentUserPlaylistsQuery);
|
final playlists = ref.watch(currentUserPlaylistsQuery);
|
||||||
|
|
||||||
return playlists.when(
|
return playlists.when(
|
||||||
loading: () =>
|
loading: () => const Center(child: ShimmerPlaybuttonCard(count: 7)),
|
||||||
const Center(child: CircularProgressIndicator.adaptive()),
|
|
||||||
data: (data) {
|
data: (data) {
|
||||||
Image image = Image();
|
Image image = Image();
|
||||||
image.height = 300;
|
image.height = 300;
|
||||||
|
51
lib/components/LoaderShimmers/ShimmerArtistProfile.dart
Normal file
51
lib/components/LoaderShimmers/ShimmerArtistProfile.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:skeleton_text/skeleton_text.dart';
|
||||||
|
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||||
|
|
||||||
|
class ShimmerArtistProfile extends HookWidget {
|
||||||
|
const ShimmerArtistProfile({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final shimmerColor =
|
||||||
|
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||||
|
final shimmerBackgroundColor = Theme.of(context)
|
||||||
|
.extension<ShimmerColorTheme>()!
|
||||||
|
.shimmerBackgroundColor!;
|
||||||
|
|
||||||
|
final avatarWidth = useBreakpointValue(
|
||||||
|
sm: MediaQuery.of(context).size.width * 0.80,
|
||||||
|
md: MediaQuery.of(context).size.width * 0.50,
|
||||||
|
lg: MediaQuery.of(context).size.width * 0.30,
|
||||||
|
xl: MediaQuery.of(context).size.width * 0.30,
|
||||||
|
xxl: MediaQuery.of(context).size.width * 0.30,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(avatarWidth),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: avatarWidth,
|
||||||
|
height: avatarWidth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(avatarWidth),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Flexible(child: ShimmerTrackTile()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
lib/components/LoaderShimmers/ShimmerCategories.dart
Normal file
44
lib/components/LoaderShimmers/ShimmerCategories.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:skeleton_text/skeleton_text.dart';
|
||||||
|
import 'package:spotube/components/LoaderShimmers/ShimmerPlaybuttonCard.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
|
class ShimmerCategories extends StatelessWidget {
|
||||||
|
const ShimmerCategories({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final shimmerColor =
|
||||||
|
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||||
|
final shimmerBackgroundColor = Theme.of(context)
|
||||||
|
.extension<ShimmerColorTheme>()!
|
||||||
|
.shimmerBackgroundColor!;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: 150,
|
||||||
|
height: 15,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ShimmerPlaybuttonCard(count: 7),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
87
lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart
Normal file
87
lib/components/LoaderShimmers/ShimmerPlaybuttonCard.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:skeleton_text/skeleton_text.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
|
class ShimmerPlaybuttonCard extends StatelessWidget {
|
||||||
|
final int count;
|
||||||
|
const ShimmerPlaybuttonCard({Key? key, this.count = 4}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final shimmerColor =
|
||||||
|
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||||
|
final shimmerBackgroundColor = Theme.of(context)
|
||||||
|
.extension<ShimmerColorTheme>()!
|
||||||
|
.shimmerBackgroundColor!;
|
||||||
|
|
||||||
|
final card = Stack(
|
||||||
|
children: [
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: 200,
|
||||||
|
height: 220,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: 200,
|
||||||
|
height: 180,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: 150,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: List.generate(
|
||||||
|
count,
|
||||||
|
(_) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
child: card,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
85
lib/components/LoaderShimmers/ShimmerTrackTile.dart
Normal file
85
lib/components/LoaderShimmers/ShimmerTrackTile.dart
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:skeleton_text/skeleton_text.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
|
class ShimmerTrackTile extends StatelessWidget {
|
||||||
|
const ShimmerTrackTile({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final shimmerColor =
|
||||||
|
Theme.of(context).extension<ShimmerColorTheme>()!.shimmerColor!;
|
||||||
|
final shimmerBackgroundColor = Theme.of(context)
|
||||||
|
.extension<ShimmerColorTheme>()!
|
||||||
|
.shimmerBackgroundColor!;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 30),
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: 5,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
height: 15,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SkeletonAnimation(
|
||||||
|
shimmerColor: shimmerColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
shimmerDuration: 1000,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * .8),
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: shimmerBackgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(top: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -141,7 +141,9 @@ class SyncedLyrics extends HookConsumerWidget {
|
|||||||
lyricSlice.text,
|
lyricSlice.text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
// indicating the active state of that lyric slice
|
// indicating the active state of that lyric slice
|
||||||
color: isActive ? Colors.green : null,
|
color: isActive
|
||||||
|
? Theme.of(context).primaryColor
|
||||||
|
: null,
|
||||||
fontWeight: isActive ? FontWeight.bold : null,
|
fontWeight: isActive ? FontWeight.bold : null,
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
),
|
),
|
||||||
|
@ -37,56 +37,65 @@ class PlayerOverlay extends HookConsumerWidget {
|
|||||||
right: (breakpoint.isMd ? 10 : 5),
|
right: (breakpoint.isMd ? 10 : 5),
|
||||||
left: (breakpoint.isSm ? 5 : 80),
|
left: (breakpoint.isSm ? 5 : 80),
|
||||||
bottom: (breakpoint.isSm ? 63 : 10),
|
bottom: (breakpoint.isSm ? 63 : 10),
|
||||||
child: AnimatedContainer(
|
child: GestureDetector(
|
||||||
duration: const Duration(milliseconds: 500),
|
onVerticalDragEnd: (details) {
|
||||||
width: MediaQuery.of(context).size.width,
|
int sensitivity = 8;
|
||||||
height: 50,
|
if (details.primaryVelocity != null &&
|
||||||
decoration: BoxDecoration(
|
details.primaryVelocity! < -sensitivity) {
|
||||||
color: paletteColor.color,
|
GoRouter.of(context).push("/player");
|
||||||
borderRadius: BorderRadius.circular(5),
|
}
|
||||||
),
|
},
|
||||||
child: Material(
|
child: AnimatedContainer(
|
||||||
type: MaterialType.transparency,
|
duration: const Duration(milliseconds: 500),
|
||||||
child: Row(
|
width: MediaQuery.of(context).size.width,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
height: 50,
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
Expanded(
|
color: paletteColor.color,
|
||||||
child: MouseRegion(
|
borderRadius: BorderRadius.circular(5),
|
||||||
cursor: SystemMouseCursors.click,
|
),
|
||||||
child: GestureDetector(
|
child: Material(
|
||||||
onTap: () => GoRouter.of(context).push("/player"),
|
type: MaterialType.transparency,
|
||||||
child: PlayerTrackDetails(
|
child: Row(
|
||||||
albumArt: albumArt,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
color: paletteColor.bodyTextColor,
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => GoRouter.of(context).push("/player"),
|
||||||
|
child: PlayerTrackDetails(
|
||||||
|
albumArt: albumArt,
|
||||||
|
color: paletteColor.bodyTextColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
IconButton(
|
||||||
IconButton(
|
icon: const Icon(Icons.skip_previous_rounded),
|
||||||
icon: const Icon(Icons.skip_previous_rounded),
|
color: paletteColor.bodyTextColor,
|
||||||
|
onPressed: () {
|
||||||
|
onPrevious();
|
||||||
|
}),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
playback.isPlaying
|
||||||
|
? Icons.pause_rounded
|
||||||
|
: Icons.play_arrow_rounded,
|
||||||
|
),
|
||||||
color: paletteColor.bodyTextColor,
|
color: paletteColor.bodyTextColor,
|
||||||
onPressed: () {
|
onPressed: _playOrPause,
|
||||||
onPrevious();
|
|
||||||
}),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
playback.isPlaying
|
|
||||||
? Icons.pause_rounded
|
|
||||||
: Icons.play_arrow_rounded,
|
|
||||||
),
|
),
|
||||||
color: paletteColor.bodyTextColor,
|
IconButton(
|
||||||
onPressed: _playOrPause,
|
icon: const Icon(Icons.skip_next_rounded),
|
||||||
),
|
onPressed: () => onNext(),
|
||||||
IconButton(
|
color: paletteColor.bodyTextColor,
|
||||||
icon: const Icon(Icons.skip_next_rounded),
|
),
|
||||||
onPressed: () => onNext(),
|
],
|
||||||
color: paletteColor.bodyTextColor,
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -30,7 +30,7 @@ class PlayerTrackDetails extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 50,
|
height: 50,
|
||||||
width: 50,
|
width: 50,
|
||||||
color: Colors.green[400],
|
color: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -74,7 +74,7 @@ class PlayerView extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: const PageWindowTitleBar(
|
appBar: const PageWindowTitleBar(
|
||||||
leading: BackButton(),
|
leading: BackButton(),
|
||||||
transparent: true,
|
backgroundColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
backgroundColor: paletteColor.color,
|
backgroundColor: paletteColor.color,
|
||||||
body: Column(
|
body: Column(
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||||
import 'package:spotube/models/Logger.dart';
|
import 'package:spotube/models/Logger.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
@ -23,7 +24,7 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
playPlaylist(Playback playback, List<Track> tracks,
|
playPlaylist(Playback playback, List<Track> tracks,
|
||||||
{Track? currentTrack}) async {
|
{Track? currentTrack}) async {
|
||||||
currentTrack ??= tracks.first;
|
currentTrack ??= tracks.first;
|
||||||
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||||
playback.currentPlaylist?.id == playlist.id;
|
playback.currentPlaylist?.id == playlist.id;
|
||||||
if (!isPlaylistPlaying) {
|
if (!isPlaylistPlaying) {
|
||||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
playback.setCurrentPlaylist = CurrentPlaylist(
|
||||||
@ -52,117 +53,90 @@ class PlaylistView extends HookConsumerWidget {
|
|||||||
final meSnapshot = ref.watch(currentUserQuery);
|
final meSnapshot = ref.watch(currentUserQuery);
|
||||||
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
final tracksSnapshot = ref.watch(playlistTracksQuery(playlist.id!));
|
||||||
|
|
||||||
return SafeArea(
|
final titleImage =
|
||||||
child: Scaffold(
|
useMemoized(() => imageToUrlString(playlist.images), [playlist.images]);
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
PageWindowTitleBar(
|
|
||||||
leading: Row(
|
|
||||||
children: [
|
|
||||||
// nav back
|
|
||||||
const BackButton(),
|
|
||||||
// heart playlist
|
|
||||||
if (auth.isLoggedIn)
|
|
||||||
meSnapshot.when(
|
|
||||||
data: (me) {
|
|
||||||
final query = playlistIsFollowedQuery(jsonEncode(
|
|
||||||
{"playlistId": playlist.id, "userId": me.id!}));
|
|
||||||
final followingSnapshot = ref.watch(query);
|
|
||||||
|
|
||||||
return followingSnapshot.when(
|
final color = usePaletteGenerator(
|
||||||
data: (isFollowing) {
|
context,
|
||||||
return HeartButton(
|
titleImage,
|
||||||
isLiked: isFollowing,
|
).dominantColor;
|
||||||
icon: playlist.owner?.id != null &&
|
|
||||||
me.id == playlist.owner?.id
|
|
||||||
? Icons.delete_outline_rounded
|
|
||||||
: null,
|
|
||||||
onPressed: () async {
|
|
||||||
try {
|
|
||||||
isFollowing
|
|
||||||
? spotify.playlists
|
|
||||||
.unfollowPlaylist(playlist.id!)
|
|
||||||
: spotify.playlists
|
|
||||||
.followPlaylist(playlist.id!);
|
|
||||||
} catch (e, stack) {
|
|
||||||
logger.e("FollowButton.onPressed", e, stack);
|
|
||||||
} finally {
|
|
||||||
ref.refresh(query);
|
|
||||||
ref.refresh(currentUserPlaylistsQuery);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, _) => Text("Error $error"),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error: (error, _) => Text("Error $error"),
|
|
||||||
loading: () => const CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
|
|
||||||
IconButton(
|
return TrackCollectionView(
|
||||||
icon: const Icon(Icons.share_rounded),
|
id: playlist.id!,
|
||||||
onPressed: () {
|
isPlaying: isPlaylistPlaying,
|
||||||
final data =
|
title: playlist.name!,
|
||||||
"https://open.spotify.com/playlist/${playlist.id}";
|
titleImage: titleImage,
|
||||||
Clipboard.setData(
|
tracksSnapshot: tracksSnapshot,
|
||||||
ClipboardData(text: data),
|
description: playlist.description,
|
||||||
).then((_) {
|
isOwned: playlist.owner?.id != null &&
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
playlist.owner!.id == meSnapshot.asData?.value.id,
|
||||||
SnackBar(
|
onPlay: ([track]) {
|
||||||
width: 300,
|
if (tracksSnapshot.asData?.value != null) {
|
||||||
behavior: SnackBarBehavior.floating,
|
playPlaylist(
|
||||||
content: Text(
|
playback,
|
||||||
"Copied $data to clipboard",
|
tracksSnapshot.asData!.value,
|
||||||
textAlign: TextAlign.center,
|
currentTrack: track,
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
);
|
},
|
||||||
});
|
showShare: playlist.id != "user-liked-tracks",
|
||||||
},
|
onShare: () {
|
||||||
),
|
final data = "https://open.spotify.com/playlist/${playlist.id}";
|
||||||
// play playlist
|
Clipboard.setData(
|
||||||
IconButton(
|
ClipboardData(text: data),
|
||||||
icon: Icon(
|
).then((_) {
|
||||||
isPlaylistPlaying
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
? Icons.stop_rounded
|
SnackBar(
|
||||||
: Icons.play_arrow_rounded,
|
width: 300,
|
||||||
),
|
behavior: SnackBarBehavior.floating,
|
||||||
onPressed: tracksSnapshot.asData?.value != null
|
content: Text(
|
||||||
? () => playPlaylist(
|
"Copied $data to clipboard",
|
||||||
playback,
|
textAlign: TextAlign.center,
|
||||||
tracksSnapshot.asData!.value,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
);
|
||||||
child: Text(playlist.name!,
|
});
|
||||||
style: Theme.of(context).textTheme.headline4),
|
},
|
||||||
),
|
heartBtn: (auth.isLoggedIn && playlist.id != "user-liked-tracks"
|
||||||
tracksSnapshot.when(
|
? meSnapshot.when(
|
||||||
data: (tracks) {
|
data: (me) {
|
||||||
return TracksTableView(
|
final query = playlistIsFollowedQuery(
|
||||||
tracks,
|
jsonEncode({"playlistId": playlist.id, "userId": me.id!}));
|
||||||
onTrackPlayButtonPressed: (currentTrack) => playPlaylist(
|
final followingSnapshot = ref.watch(query);
|
||||||
playback,
|
|
||||||
tracks,
|
return followingSnapshot.when(
|
||||||
currentTrack: currentTrack,
|
data: (isFollowing) {
|
||||||
),
|
return HeartButton(
|
||||||
playlistId: playlist.id,
|
isLiked: isFollowing,
|
||||||
userPlaylist: playlist.owner?.id != null &&
|
color: color?.titleTextColor,
|
||||||
playlist.owner!.id == meSnapshot.asData?.value.id,
|
icon: playlist.owner?.id != null &&
|
||||||
|
me.id == playlist.owner?.id
|
||||||
|
? Icons.delete_outline_rounded
|
||||||
|
: null,
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
isFollowing
|
||||||
|
? await spotify.playlists
|
||||||
|
.unfollowPlaylist(playlist.id!)
|
||||||
|
: await spotify.playlists
|
||||||
|
.followPlaylist(playlist.id!);
|
||||||
|
} catch (e, stack) {
|
||||||
|
logger.e("FollowButton.onPressed", e, stack);
|
||||||
|
} finally {
|
||||||
|
ref.refresh(query);
|
||||||
|
ref.refresh(currentUserPlaylistsQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => Text("Error $error"),
|
||||||
|
loading: () => const CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
error: (error, _) => Text("Error $error"),
|
error: (error, _) => Text("Error $error"),
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const CircularProgressIndicator(),
|
||||||
),
|
)
|
||||||
],
|
: null),
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ class About extends HookWidget {
|
|||||||
final info = usePackageInfo(
|
final info = usePackageInfo(
|
||||||
appName: "Spotube",
|
appName: "Spotube",
|
||||||
packageName: "oss.krtirtho.Spotube",
|
packageName: "oss.krtirtho.Spotube",
|
||||||
version: "2.2.0");
|
version: "2.2.1");
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: const Text("About Spotube"),
|
title: const Text("About Spotube"),
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:spotube/components/Settings/About.dart';
|
import 'package:spotube/components/Settings/About.dart';
|
||||||
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
|
import 'package:spotube/components/Settings/ColorSchemePickerDialog.dart';
|
||||||
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
|
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
|
||||||
@ -133,22 +132,43 @@ class Settings extends HookConsumerWidget {
|
|||||||
onTap: pickColorScheme(ColorSchemeType.background),
|
onTap: pickColorScheme(ColorSchemeType.background),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ListTile(
|
Padding(
|
||||||
title:
|
padding: const EdgeInsets.all(15),
|
||||||
const Text("Market Place (Recommendation Country)"),
|
child: Wrap(
|
||||||
horizontalTitleGap: 10,
|
alignment: WrapAlignment.spaceBetween,
|
||||||
trailing: DropdownButton(
|
children: [
|
||||||
value: preferences.recommendationMarket,
|
Column(
|
||||||
items: spotifyMarkets
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.map((country) => (DropdownMenuItem(
|
children: [
|
||||||
child: Text(country),
|
Text(
|
||||||
value: country,
|
"Market Place",
|
||||||
)))
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
.toList(),
|
),
|
||||||
onChanged: (value) {
|
Text(
|
||||||
if (value == null) return;
|
"Recommendation Country",
|
||||||
preferences.setRecommendationMarket(value as String);
|
style: Theme.of(context).textTheme.caption,
|
||||||
},
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
DropdownButton(
|
||||||
|
value: preferences.recommendationMarket,
|
||||||
|
items: spotifyMarkets
|
||||||
|
.map(
|
||||||
|
(country) => (DropdownMenuItem(
|
||||||
|
child: Text(country.last),
|
||||||
|
value: country.first,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
preferences.setRecommendationMarket(
|
||||||
|
value as String,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
@ -4,9 +4,11 @@ class HeartButton extends StatelessWidget {
|
|||||||
final bool isLiked;
|
final bool isLiked;
|
||||||
final void Function() onPressed;
|
final void Function() onPressed;
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
|
final Color? color;
|
||||||
const HeartButton({
|
const HeartButton({
|
||||||
required this.isLiked,
|
required this.isLiked,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
|
this.color,
|
||||||
this.icon,
|
this.icon,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -19,7 +21,7 @@ class HeartButton extends StatelessWidget {
|
|||||||
(!isLiked
|
(!isLiked
|
||||||
? Icons.favorite_outline_rounded
|
? Icons.favorite_outline_rounded
|
||||||
: Icons.favorite_rounded),
|
: Icons.favorite_rounded),
|
||||||
color: isLiked ? Theme.of(context).primaryColor : null,
|
color: isLiked ? Theme.of(context).primaryColor : color,
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
|
30
lib/components/Shared/NotFound.dart
Normal file
30
lib/components/Shared/NotFound.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class NotFound extends StatelessWidget {
|
||||||
|
const NotFound({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 150,
|
||||||
|
width: 150,
|
||||||
|
child: Image.asset("assets/empty_box.png"),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text("Nothing found", style: Theme.of(context).textTheme.headline6),
|
||||||
|
Text(
|
||||||
|
"The box is empty",
|
||||||
|
style: Theme.of(context).textTheme.subtitle1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,11 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TitleBarActionButtons extends StatelessWidget {
|
class TitleBarActionButtons extends StatelessWidget {
|
||||||
const TitleBarActionButtons({Key? key}) : super(key: key);
|
final Color? color;
|
||||||
|
const TitleBarActionButtons({
|
||||||
|
Key? key,
|
||||||
|
this.color,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -18,7 +22,10 @@ class TitleBarActionButtons extends StatelessWidget {
|
|||||||
foregroundColor:
|
foregroundColor:
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.minimize_rounded)),
|
child: Icon(
|
||||||
|
Icons.minimize_rounded,
|
||||||
|
color: color,
|
||||||
|
)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
appWindow.maximizeOrRestore();
|
appWindow.maximizeOrRestore();
|
||||||
@ -27,14 +34,14 @@ class TitleBarActionButtons extends StatelessWidget {
|
|||||||
foregroundColor:
|
foregroundColor:
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.crop_square_rounded)),
|
child: Icon(Icons.crop_square_rounded, color: color)),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
appWindow.close();
|
appWindow.close();
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
foregroundColor:
|
foregroundColor: MaterialStateProperty.all(
|
||||||
MaterialStateProperty.all(Theme.of(context).iconTheme.color),
|
color ?? Theme.of(context).iconTheme.color),
|
||||||
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
overlayColor: MaterialStateProperty.all(Colors.redAccent),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@ -49,12 +56,14 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
implements PreferredSizeWidget {
|
implements PreferredSizeWidget {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final Widget? center;
|
final Widget? center;
|
||||||
final bool transparent;
|
final Color? backgroundColor;
|
||||||
|
final Color? foregroundColor;
|
||||||
const PageWindowTitleBar({
|
const PageWindowTitleBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.center,
|
this.center,
|
||||||
this.transparent = false,
|
this.backgroundColor,
|
||||||
|
this.foregroundColor,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => Size.fromHeight(
|
Size get preferredSize => Size.fromHeight(
|
||||||
@ -76,7 +85,7 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
}
|
}
|
||||||
return WindowTitleBarBox(
|
return WindowTitleBarBox(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: !transparent ? Theme.of(context).scaffoldBackgroundColor : null,
|
color: backgroundColor,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (Platform.isMacOS)
|
if (Platform.isMacOS)
|
||||||
@ -86,7 +95,7 @@ class PageWindowTitleBar extends StatelessWidget
|
|||||||
if (leading != null) leading!,
|
if (leading != null) leading!,
|
||||||
Expanded(child: MoveWindow(child: Center(child: center))),
|
Expanded(child: MoveWindow(child: Center(child: center))),
|
||||||
if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid)
|
if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid)
|
||||||
const TitleBarActionButtons()
|
TitleBarActionButtons(color: foregroundColor)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
240
lib/components/Shared/TrackCollectionView.dart
Normal file
240
lib/components/Shared/TrackCollectionView.dart
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
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/helpers/simple-track-to-track.dart';
|
||||||
|
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||||
|
import 'package:spotube/models/Logger.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
class TrackCollectionView extends HookConsumerWidget {
|
||||||
|
final logger = getLogger(TrackCollectionView);
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final AsyncValue<List<TrackSimple>> tracksSnapshot;
|
||||||
|
final String titleImage;
|
||||||
|
final bool isPlaying;
|
||||||
|
final void Function([Track? currentTrack]) onPlay;
|
||||||
|
final void Function() onShare;
|
||||||
|
final Widget? heartBtn;
|
||||||
|
final AlbumSimple? album;
|
||||||
|
|
||||||
|
final bool showShare;
|
||||||
|
final bool isOwned;
|
||||||
|
TrackCollectionView({
|
||||||
|
required this.title,
|
||||||
|
required this.id,
|
||||||
|
required this.tracksSnapshot,
|
||||||
|
required this.titleImage,
|
||||||
|
required this.isPlaying,
|
||||||
|
required this.onPlay,
|
||||||
|
required this.onShare,
|
||||||
|
this.heartBtn,
|
||||||
|
this.album,
|
||||||
|
this.description,
|
||||||
|
this.showShare = true,
|
||||||
|
this.isOwned = false,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final color = usePaletteGenerator(
|
||||||
|
context,
|
||||||
|
titleImage,
|
||||||
|
).dominantColor;
|
||||||
|
|
||||||
|
final List<Widget> buttons = [
|
||||||
|
if (showShare)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.share_rounded,
|
||||||
|
color: color?.titleTextColor,
|
||||||
|
),
|
||||||
|
onPressed: onShare,
|
||||||
|
),
|
||||||
|
if (heartBtn != null) heartBtn!,
|
||||||
|
|
||||||
|
// play playlist
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor:
|
||||||
|
MaterialStateProperty.all(Theme.of(context).primaryColor),
|
||||||
|
shape: MaterialStateProperty.all(
|
||||||
|
const CircleBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isPlaying ? Icons.stop_rounded : Icons.play_arrow_rounded,
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
|
),
|
||||||
|
onPressed: tracksSnapshot.asData?.value != null ? onPlay : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final controller = useScrollController();
|
||||||
|
|
||||||
|
final collapsed = useState(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
listener() {
|
||||||
|
if (controller.position.pixels >= 400 && !collapsed.value) {
|
||||||
|
collapsed.value = true;
|
||||||
|
} else if (controller.position.pixels < 400 && collapsed.value) {
|
||||||
|
collapsed.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.addListener(listener);
|
||||||
|
|
||||||
|
return () => controller.removeListener(listener);
|
||||||
|
}, [collapsed.value]);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: PageWindowTitleBar(
|
||||||
|
backgroundColor:
|
||||||
|
tracksSnapshot.asData?.value != null ? color?.color : null,
|
||||||
|
foregroundColor: tracksSnapshot.asData?.value != null
|
||||||
|
? color?.titleTextColor
|
||||||
|
: null,
|
||||||
|
leading: Row(
|
||||||
|
children: [
|
||||||
|
BackButton(
|
||||||
|
color: tracksSnapshot.asData?.value != null
|
||||||
|
? color?.titleTextColor
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: tracksSnapshot.when(
|
||||||
|
data: (tracks) {
|
||||||
|
return CustomScrollView(
|
||||||
|
controller: controller,
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
actions: collapsed.value ? buttons : null,
|
||||||
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
|
expandedHeight: 400,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
primary: true,
|
||||||
|
title: collapsed.value
|
||||||
|
? Text(
|
||||||
|
title,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.headline4?.copyWith(
|
||||||
|
color: color?.titleTextColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
backgroundColor: color?.color.withOpacity(0.8),
|
||||||
|
flexibleSpace: LayoutBuilder(builder: (context, constrains) {
|
||||||
|
return FlexibleSpaceBar(
|
||||||
|
background: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
color?.color ?? Colors.transparent,
|
||||||
|
Theme.of(context).canvasColor,
|
||||||
|
],
|
||||||
|
begin: const FractionalOffset(0, 0),
|
||||||
|
end: const FractionalOffset(0, 1),
|
||||||
|
tileMode: TileMode.clamp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 20,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
runAlignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
constraints:
|
||||||
|
const BoxConstraints(maxHeight: 200),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: titleImage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headline4
|
||||||
|
?.copyWith(
|
||||||
|
color: color?.titleTextColor,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (description != null)
|
||||||
|
Text(
|
||||||
|
description!,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge
|
||||||
|
?.copyWith(
|
||||||
|
color: color?.bodyTextColor,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: buttons,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
TracksTableView(
|
||||||
|
tracks is! List<Track>
|
||||||
|
? tracks
|
||||||
|
.map((track) => simpleTrackToTrack(track, album!))
|
||||||
|
.toList()
|
||||||
|
: tracks,
|
||||||
|
onTrackPlayButtonPressed: onPlay,
|
||||||
|
playlistId: id,
|
||||||
|
userPlaylist: isOwned,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => Text("Error $error"),
|
||||||
|
loading: () => const ShimmerTrackTile(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -175,7 +175,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 40,
|
width: 40,
|
||||||
color: Colors.green[300],
|
color: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
imageUrl: thumbnailUrl!,
|
imageUrl: thumbnailUrl!,
|
||||||
|
@ -12,12 +12,15 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
final List<Track> tracks;
|
final List<Track> tracks;
|
||||||
final bool userPlaylist;
|
final bool userPlaylist;
|
||||||
final String? playlistId;
|
final String? playlistId;
|
||||||
|
|
||||||
|
final Widget? heading;
|
||||||
const TracksTableView(
|
const TracksTableView(
|
||||||
this.tracks, {
|
this.tracks, {
|
||||||
Key? key,
|
Key? key,
|
||||||
this.onTrackPlayButtonPressed,
|
this.onTrackPlayButtonPressed,
|
||||||
this.userPlaylist = false,
|
this.userPlaylist = false,
|
||||||
this.playlistId,
|
this.playlistId,
|
||||||
|
this.heading,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,10 +31,79 @@ class TracksTableView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final breakpoint = useBreakpoints();
|
final breakpoint = useBreakpoints();
|
||||||
|
|
||||||
return Expanded(
|
return SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
if (heading != null) heading!,
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
"#",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: tableHeadStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Title",
|
||||||
|
style: tableHeadStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// used alignment of this table-head
|
||||||
|
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
|
||||||
|
const SizedBox(width: 100),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Album",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: tableHeadStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
if (!breakpoint.isSm) ...[
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text("Time", style: tableHeadStyle),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
|
const SizedBox(width: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...tracks.asMap().entries.map((track) {
|
||||||
|
String? thumbnailUrl = imageToUrlString(
|
||||||
|
track.value.album?.images,
|
||||||
|
index: (track.value.album?.images?.length ?? 1) - 1,
|
||||||
|
);
|
||||||
|
String duration =
|
||||||
|
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
|
return TrackTile(
|
||||||
|
playback,
|
||||||
|
playlistId: playlistId,
|
||||||
|
track: track,
|
||||||
|
duration: duration,
|
||||||
|
thumbnailUrl: thumbnailUrl,
|
||||||
|
userPlaylist: userPlaylist,
|
||||||
|
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
|
||||||
|
);
|
||||||
|
}).toList()
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
|
if (heading != null) heading!,
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
34
lib/extensions/ShimmerColorTheme.dart
Normal file
34
lib/extensions/ShimmerColorTheme.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ShimmerColorTheme extends ThemeExtension<ShimmerColorTheme> {
|
||||||
|
final Color? shimmerColor;
|
||||||
|
final Color? shimmerBackgroundColor;
|
||||||
|
|
||||||
|
ShimmerColorTheme({
|
||||||
|
this.shimmerBackgroundColor,
|
||||||
|
this.shimmerColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<ShimmerColorTheme> copyWith(
|
||||||
|
{Color? shimmerColor, Color? shimmerBackgroundColor}) {
|
||||||
|
return ShimmerColorTheme(
|
||||||
|
shimmerBackgroundColor:
|
||||||
|
shimmerBackgroundColor ?? this.shimmerBackgroundColor,
|
||||||
|
shimmerColor: shimmerColor ?? this.shimmerColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeExtension<ShimmerColorTheme> lerp(
|
||||||
|
ThemeExtension<ShimmerColorTheme>? other, double t) {
|
||||||
|
if (other is! ShimmerColorTheme) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return ShimmerColorTheme(
|
||||||
|
shimmerBackgroundColor:
|
||||||
|
Color.lerp(shimmerBackgroundColor, other.shimmerBackgroundColor, t),
|
||||||
|
shimmerColor: Color.lerp(shimmerColor, other.shimmerColor, t),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -38,3 +38,30 @@ PaletteColor usePaletteColor(
|
|||||||
|
|
||||||
return paletteColor;
|
return paletteColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PaletteGenerator usePaletteGenerator(
|
||||||
|
BuildContext context,
|
||||||
|
String imageUrl,
|
||||||
|
) {
|
||||||
|
final palette = useState(PaletteGenerator.fromColors([]));
|
||||||
|
final mounted = useIsMounted();
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||||
|
final newPalette = await PaletteGenerator.fromImageProvider(
|
||||||
|
CachedNetworkImageProvider(
|
||||||
|
imageUrl,
|
||||||
|
cacheKey: imageUrl,
|
||||||
|
maxHeight: 50,
|
||||||
|
maxWidth: 50,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!mounted()) return;
|
||||||
|
|
||||||
|
palette.value = newPalette;
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}, [imageUrl]);
|
||||||
|
|
||||||
|
return palette.value;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,60 @@
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
extension AlbumJson on AlbumSimple {
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"albumType": albumType,
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"images": images
|
||||||
|
?.map((image) => {
|
||||||
|
"height": image.height,
|
||||||
|
"url": image.url,
|
||||||
|
"width": image.width,
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ArtistJson on ArtistSimple {
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"href": href,
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"type": type,
|
||||||
|
"uri": uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TrackJson on Track {
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"album": album?.toJson(),
|
||||||
|
"artists": artists?.map((artist) => artist.toJson()).toList(),
|
||||||
|
"availableMarkets": availableMarkets,
|
||||||
|
"discNumber": discNumber,
|
||||||
|
"duration": duration.toString(),
|
||||||
|
"durationMs": durationMs,
|
||||||
|
"explicit": explicit,
|
||||||
|
// "externalIds": externalIds,
|
||||||
|
// "externalUrls": externalUrls,
|
||||||
|
"href": href,
|
||||||
|
"id": id,
|
||||||
|
"isPlayable": isPlayable,
|
||||||
|
// "linkedFrom": linkedFrom,
|
||||||
|
"name": name,
|
||||||
|
"popularity": popularity,
|
||||||
|
"previewUrl": previewUrl,
|
||||||
|
"trackNumber": trackNumber,
|
||||||
|
"type": type,
|
||||||
|
"uri": uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CurrentPlaylist {
|
class CurrentPlaylist {
|
||||||
List<Track>? _tempTrack;
|
List<Track>? _tempTrack;
|
||||||
List<Track> tracks;
|
List<Track> tracks;
|
||||||
@ -14,6 +69,16 @@ class CurrentPlaylist {
|
|||||||
required this.thumbnail,
|
required this.thumbnail,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static CurrentPlaylist fromJson(Map<String, dynamic> map) {
|
||||||
|
return CurrentPlaylist(
|
||||||
|
id: map["id"],
|
||||||
|
tracks: List.castFrom<dynamic, Track>(
|
||||||
|
map["tracks"].map((track) => Track.fromJson(track)).toList()),
|
||||||
|
name: map["name"],
|
||||||
|
thumbnail: map["thumbnail"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
List<String> get trackIds => tracks.map((e) => e.id!).toList();
|
||||||
|
|
||||||
bool shuffle() {
|
bool shuffle() {
|
||||||
@ -35,4 +100,13 @@ class CurrentPlaylist {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"tracks": tracks.map((track) => track.toJson()).toList(),
|
||||||
|
"thumbnail": thumbnail,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,186 +1,188 @@
|
|||||||
|
// Country Codes contributed by momobobe <https://github.com/momobobe>
|
||||||
|
|
||||||
final spotifyMarkets = [
|
final spotifyMarkets = [
|
||||||
"AD",
|
["AL", "Albania (AL)"],
|
||||||
"AE",
|
["DZ", "Algeria (DZ)"],
|
||||||
"AG",
|
["AD", "Andorra (AD)"],
|
||||||
"AL",
|
["AO", "Angola (AO)"],
|
||||||
"AM",
|
["AG", "Antigua and Barbuda (AG)"],
|
||||||
"AO",
|
["AR", "Argentina (AR)"],
|
||||||
"AR",
|
["AM", "Armenia (AM)"],
|
||||||
"AT",
|
["AU", "Australia (AU)"],
|
||||||
"AU",
|
["AT", "Austria (AT)"],
|
||||||
"AZ",
|
["AZ", "Azerbaijan (AZ)"],
|
||||||
"BA",
|
["BH", "Bahrain (BH)"],
|
||||||
"BB",
|
["BD", "Bangladesh (BD)"],
|
||||||
"BD",
|
["BB", "Barbados (BB)"],
|
||||||
"BE",
|
["BY", "Belarus (BY)"],
|
||||||
"BF",
|
["BE", "Belgium (BE)"],
|
||||||
"BG",
|
["BZ", "Belize (BZ)"],
|
||||||
"BH",
|
["BJ", "Benin (BJ)"],
|
||||||
"BI",
|
["BT", "Bhutan (BT)"],
|
||||||
"BJ",
|
["BO", "Bolivia (BO)"],
|
||||||
"BN",
|
["BA", "Bosnia and Herzegovina (BA)"],
|
||||||
"BO",
|
["BW", "Botswana (BW)"],
|
||||||
"BR",
|
["BR", "Brazil (BR)"],
|
||||||
"BS",
|
["BN", "Brunei Darussalam (BN)"],
|
||||||
"BT",
|
["BG", "Bulgaria (BG)"],
|
||||||
"BW",
|
["BF", "Burkina Faso (BF)"],
|
||||||
"BY",
|
["BI", "Burundi (BI)"],
|
||||||
"BZ",
|
["CV", "Cabo Verde / Cape Verde (CV)"],
|
||||||
"CA",
|
["KH", "Cambodia (KH)"],
|
||||||
"CD",
|
["CM", "Cameroon (CM)"],
|
||||||
"CG",
|
["CA", "Canada (CA)"],
|
||||||
"CH",
|
["TD", "Chad (TD)"],
|
||||||
"CI",
|
["CL", "Chile (CL)"],
|
||||||
"CL",
|
["CO", "Colombia (CO)"],
|
||||||
"CM",
|
["KM", "Comoros (KM)"],
|
||||||
"CO",
|
["CR", "Costa Rica (CR)"],
|
||||||
"CR",
|
["HR", "Croatia (HR)"],
|
||||||
"CV",
|
["CW", "Curaçao (CW)"],
|
||||||
"CW",
|
["CY", "Cyprus (CY)"],
|
||||||
"CY",
|
["CZ", "Czech Republic (CZ)"],
|
||||||
"CZ",
|
["CI", "Côte d'Ivoire / Ivory Coast (CI)"],
|
||||||
"DE",
|
["CD", "Democratic Republic of the Congo (CD)"],
|
||||||
"DJ",
|
["DK", "Denmark (DK)"],
|
||||||
"DK",
|
["DJ", "Djibouti (DJ)"],
|
||||||
"DM",
|
["DM", "Dominica (DM)"],
|
||||||
"DO",
|
["DO", "Dominican Republic (DO)"],
|
||||||
"DZ",
|
["EC", "Ecuador (EC)"],
|
||||||
"EC",
|
["EG", "Egypt (EG)"],
|
||||||
"EE",
|
["SV", "El Salvador (SV)"],
|
||||||
"EG",
|
["GQ", "Equatorial Guinea (GQ)"],
|
||||||
"ES",
|
["EE", "Estonia (EE)"],
|
||||||
"FI",
|
["SZ", "Eswatini (SZ)"],
|
||||||
"FJ",
|
["FJ", "Fiji (FJ)"],
|
||||||
"FM",
|
["FI", "Finland (FI)"],
|
||||||
"FR",
|
["FR", "France (FR)"],
|
||||||
"GA",
|
["GA", "Gabon (GA)"],
|
||||||
"GB",
|
["GE", "Georgia (GE)"],
|
||||||
"GD",
|
["DE", "Germany (DE)"],
|
||||||
"GE",
|
["GH", "Ghana (GH)"],
|
||||||
"GH",
|
["GR", "Greece (GR)"],
|
||||||
"GM",
|
["GD", "Grenada (GD)"],
|
||||||
"GN",
|
["GT", "Guatemala (GT)"],
|
||||||
"GQ",
|
["GN", "Guinea (GN)"],
|
||||||
"GR",
|
["GW", "Guinea-Bissau (GW)"],
|
||||||
"GT",
|
["GY", "Guyana (GY)"],
|
||||||
"GW",
|
["HT", "Haiti (HT)"],
|
||||||
"GY",
|
["HN", "Honduras (HN)"],
|
||||||
"HK",
|
["HK", "Hong Kong (HK)"],
|
||||||
"HN",
|
["HU", "Hungary (HU)"],
|
||||||
"HR",
|
["IS", "Iceland (IS)"],
|
||||||
"HT",
|
["IN", "India (IN)"],
|
||||||
"HU",
|
["ID", "Indonesia (ID)"],
|
||||||
"ID",
|
["IQ", "Iraq (IQ)"],
|
||||||
"IE",
|
["IE", "Ireland (IE)"],
|
||||||
"IL",
|
["IL", "Israel (IL)"],
|
||||||
"IN",
|
["IT", "Italy (IT)"],
|
||||||
"IQ",
|
["JM", "Jamaica (JM)"],
|
||||||
"IS",
|
["JP", "Japan (JP)"],
|
||||||
"IT",
|
["JO", "Jordan (JO)"],
|
||||||
"JM",
|
["KZ", "Kazakhstan (KZ)"],
|
||||||
"JO",
|
["KE", "Kenya (KE)"],
|
||||||
"JP",
|
["KI", "Kiribati (KI)"],
|
||||||
"KE",
|
["XK", "Kosovo (XK)"],
|
||||||
"KG",
|
["KW", "Kuwait (KW)"],
|
||||||
"KH",
|
["KG", "Kyrgyzstan (KG)"],
|
||||||
"KI",
|
["LA", "Laos (LA)"],
|
||||||
"KM",
|
["LV", "Latvia (LV)"],
|
||||||
"KN",
|
["LB", "Lebanon (LB)"],
|
||||||
"KR",
|
["LS", "Lesotho (LS)"],
|
||||||
"KW",
|
["LR", "Liberia (LR)"],
|
||||||
"KZ",
|
["LY", "Libya (LY)"],
|
||||||
"LA",
|
["LI", "Liechtenstein (LI)"],
|
||||||
"LB",
|
["LT", "Lithuania (LT)"],
|
||||||
"LC",
|
["LU", "Luxembourg (LU)"],
|
||||||
"LI",
|
["MO", "Macao / Macau (MO)"],
|
||||||
"LK",
|
["MG", "Madagascar (MG)"],
|
||||||
"LR",
|
["MW", "Malawi (MW)"],
|
||||||
"LS",
|
["MY", "Malaysia (MY)"],
|
||||||
"LT",
|
["MV", "Maldives (MV)"],
|
||||||
"LU",
|
["ML", "Mali (ML)"],
|
||||||
"LV",
|
["MT", "Malta (MT)"],
|
||||||
"LY",
|
["MH", "Marshall Islands (MH)"],
|
||||||
"MA",
|
["MR", "Mauritania (MR)"],
|
||||||
"MC",
|
["MU", "Mauritius (MU)"],
|
||||||
"MD",
|
["MX", "Mexico (MX)"],
|
||||||
"ME",
|
["FM", "Micronesia (FM)"],
|
||||||
"MG",
|
["MD", "Moldova (MD)"],
|
||||||
"MH",
|
["MC", "Monaco (MC)"],
|
||||||
"MK",
|
["MN", "Mongolia (MN)"],
|
||||||
"ML",
|
["ME", "Montenegro (ME)"],
|
||||||
"MN",
|
["MA", "Morocco (MA)"],
|
||||||
"MO",
|
["MZ", "Mozambique (MZ)"],
|
||||||
"MR",
|
["NA", "Namibia (NA)"],
|
||||||
"MT",
|
["NR", "Nauru (NR)"],
|
||||||
"MU",
|
["NP", "Nepal (NP)"],
|
||||||
"MV",
|
["NL", "Netherlands (NL)"],
|
||||||
"MW",
|
["NZ", "New Zealand (NZ)"],
|
||||||
"MX",
|
["NI", "Nicaragua (NI)"],
|
||||||
"MY",
|
["NE", "Niger (NE)"],
|
||||||
"MZ",
|
["NG", "Nigeria (NG)"],
|
||||||
"NA",
|
["MK", "North Macedonia (MK)"],
|
||||||
"NE",
|
["NO", "Norway (NO)"],
|
||||||
"NG",
|
["OM", "Oman (OM)"],
|
||||||
"NI",
|
["PK", "Pakistan (PK)"],
|
||||||
"NL",
|
["PW", "Palau (PW)"],
|
||||||
"NO",
|
["PS", "Palestine (PS)"],
|
||||||
"NP",
|
["PA", "Panama (PA)"],
|
||||||
"NR",
|
["PG", "Papua New Guinea (PG)"],
|
||||||
"NZ",
|
["PY", "Paraguay (PY)"],
|
||||||
"OM",
|
["PE", "Peru (PE)"],
|
||||||
"PA",
|
["PH", "Philippines (PH)"],
|
||||||
"PE",
|
["PL", "Poland (PL)"],
|
||||||
"PG",
|
["PT", "Portugal (PT)"],
|
||||||
"PH",
|
["QA", "Qatar (QA)"],
|
||||||
"PK",
|
["CG", "Republic of the Congo (CG)"],
|
||||||
"PL",
|
["RO", "Romania (RO)"],
|
||||||
"PS",
|
["RU", "Russia (RU)"],
|
||||||
"PT",
|
["RW", "Rwanda (RW)"],
|
||||||
"PW",
|
["WS", "Samoa (WS)"],
|
||||||
"PY",
|
["SM", "San Marino (SM)"],
|
||||||
"QA",
|
["SA", "Saudi Arabia (SA)"],
|
||||||
"RO",
|
["SN", "Senegal (SN)"],
|
||||||
"RS",
|
["RS", "Serbia (RS)"],
|
||||||
"RU",
|
["SC", "Seychelles (SC)"],
|
||||||
"RW",
|
["SL", "Sierra Leone (SL)"],
|
||||||
"SA",
|
["SG", "Singapore (SG)"],
|
||||||
"SB",
|
["SK", "Slovakia (SK)"],
|
||||||
"SC",
|
["SI", "Slovenia (SI)"],
|
||||||
"SE",
|
["SB", "Solomon Islands (SB)"],
|
||||||
"SG",
|
["ZA", "South Africa (ZA)"],
|
||||||
"SI",
|
["KR", "South Korea (KR)"],
|
||||||
"SK",
|
["ES", "Spain (ES)"],
|
||||||
"SL",
|
["LK", "Sri Lanka (LK)"],
|
||||||
"SM",
|
["VC", "St Vincent and the Grenadines (VC)"],
|
||||||
"SN",
|
["KN", "St. Kitts and Nevis (KN)"],
|
||||||
"SR",
|
["LC", "St. Lucia (LC)"],
|
||||||
"ST",
|
["SR", "Suriname (SR)"],
|
||||||
"SV",
|
["SE", "Sweden (SE)"],
|
||||||
"SZ",
|
["CH", "Switzerland (CH)"],
|
||||||
"TD",
|
["ST", "São Tomé and Príncipe (ST)"],
|
||||||
"TG",
|
["TW", "Taiwan (TW)"],
|
||||||
"TH",
|
["TJ", "Tajikistan (TJ)"],
|
||||||
"TJ",
|
["TZ", "Tanzania (TZ)"],
|
||||||
"TL",
|
["TH", "Thailand (TH)"],
|
||||||
"TN",
|
["BS", "The Bahamas (BS)"],
|
||||||
"TO",
|
["GM", "The Gambia (GM)"],
|
||||||
"TR",
|
["TL", "Timor-Leste / East Timor (TL)"],
|
||||||
"TT",
|
["TG", "Togo (TG)"],
|
||||||
"TV",
|
["TO", "Tonga (TO)"],
|
||||||
"TW",
|
["TT", "Trinidad and Tobago (TT)"],
|
||||||
"TZ",
|
["TN", "Tunisia (TN)"],
|
||||||
"UA",
|
["TR", "Turkey (TR)"],
|
||||||
"UG",
|
["TV", "Tuvalu (TV)"],
|
||||||
"US",
|
["UG", "Uganda (UG)"],
|
||||||
"UY",
|
["UA", "Ukraine (UA)"],
|
||||||
"UZ",
|
["AE", "United Arab Emirates (AE)"],
|
||||||
"VC",
|
["GB", "United Kingdom (GB)"],
|
||||||
"VE",
|
["US", "United States (US)"],
|
||||||
"VN",
|
["UY", "Uruguay (UY)"],
|
||||||
"VU",
|
["UZ", "Uzbekistan (UZ)"],
|
||||||
"WS",
|
["VU", "Vanuatu (VU)"],
|
||||||
"XK",
|
["VE", "Venezuela (VE)"],
|
||||||
"ZA",
|
["VN", "Vietnam (VN)"],
|
||||||
"ZM",
|
["ZM", "Zambia (ZM)"],
|
||||||
"ZW"
|
["Z", "Zimbabwe (ZW)"],
|
||||||
];
|
];
|
||||||
|
@ -58,7 +58,7 @@ class Auth extends PersistedChangeNotifier {
|
|||||||
_refreshToken = null;
|
_refreshToken = null;
|
||||||
_expiration = null;
|
_expiration = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence(clearNullEntries: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/helpers/artist-to-string.dart';
|
import 'package:spotube/helpers/artist-to-string.dart';
|
||||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
@ -13,9 +14,10 @@ import 'package:spotube/models/Logger.dart';
|
|||||||
import 'package:spotube/provider/UserPreferences.dart';
|
import 'package:spotube/provider/UserPreferences.dart';
|
||||||
import 'package:spotube/provider/YouTube.dart';
|
import 'package:spotube/provider/YouTube.dart';
|
||||||
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
import 'package:spotube/utils/AudioPlayerHandler.dart';
|
||||||
|
import 'package:spotube/utils/PersistedChangeNotifier.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
class Playback extends ChangeNotifier {
|
class Playback extends PersistedChangeNotifier {
|
||||||
AudioSource? _currentAudioSource;
|
AudioSource? _currentAudioSource;
|
||||||
final _logger = getLogger(Playback);
|
final _logger = getLogger(Playback);
|
||||||
CurrentPlaylist? _currentPlaylist;
|
CurrentPlaylist? _currentPlaylist;
|
||||||
@ -38,13 +40,15 @@ class Playback extends ChangeNotifier {
|
|||||||
CurrentPlaylist? currentPlaylist,
|
CurrentPlaylist? currentPlaylist,
|
||||||
Track? currentTrack,
|
Track? currentTrack,
|
||||||
}) : _currentPlaylist = currentPlaylist,
|
}) : _currentPlaylist = currentPlaylist,
|
||||||
_currentTrack = currentTrack {
|
_currentTrack = currentTrack,
|
||||||
|
super() {
|
||||||
player.onNextRequest = () {
|
player.onNextRequest = () {
|
||||||
movePlaylistPositionBy(1);
|
movePlaylistPositionBy(1);
|
||||||
};
|
};
|
||||||
player.onPreviousRequest = () {
|
player.onPreviousRequest = () {
|
||||||
movePlaylistPositionBy(-1);
|
movePlaylistPositionBy(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
_init();
|
_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +56,7 @@ class Playback extends ChangeNotifier {
|
|||||||
StreamSubscription<Duration>? _positionStream;
|
StreamSubscription<Duration>? _positionStream;
|
||||||
StreamSubscription<bool>? _playingStream;
|
StreamSubscription<bool>? _playingStream;
|
||||||
|
|
||||||
void _init() {
|
void _init() async {
|
||||||
_playingStream = player.core.playingStream.listen(
|
_playingStream = player.core.playingStream.listen(
|
||||||
(playing) {
|
(playing) {
|
||||||
_isPlaying = playing;
|
_isPlaying = playing;
|
||||||
@ -119,12 +123,14 @@ class Playback extends ChangeNotifier {
|
|||||||
_logger.v("[Setting Current Track] ${track.name} - ${track.id}");
|
_logger.v("[Setting Current Track] ${track.name} - ${track.id}");
|
||||||
_currentTrack = track;
|
_currentTrack = track;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
set setCurrentPlaylist(CurrentPlaylist playlist) {
|
||||||
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}");
|
_logger.v("[Current Playlist Changed] ${playlist.name} - ${playlist.id}");
|
||||||
_currentPlaylist = playlist;
|
_currentPlaylist = playlist;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
@ -135,6 +141,7 @@ class Playback extends ChangeNotifier {
|
|||||||
_currentPlaylist = null;
|
_currentPlaylist = null;
|
||||||
_currentTrack = null;
|
_currentTrack = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence(clearNullEntries: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// sets the provided id matched track's uri\
|
/// sets the provided id matched track's uri\
|
||||||
@ -147,6 +154,7 @@ class Playback extends ChangeNotifier {
|
|||||||
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
_currentPlaylist!.tracks.indexWhere((element) => element.id == id);
|
||||||
if (index == -1) return false;
|
if (index == -1) return false;
|
||||||
_currentPlaylist!.tracks[index].uri = uri;
|
_currentPlaylist!.tracks[index].uri = uri;
|
||||||
|
updatePersistence();
|
||||||
return _currentPlaylist!.tracks[index].uri == uri;
|
return _currentPlaylist!.tracks[index].uri == uri;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
@ -170,6 +178,7 @@ class Playback extends ChangeNotifier {
|
|||||||
duration = null;
|
duration = null;
|
||||||
_currentTrack = track;
|
_currentTrack = track;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
// starts to play the newly entered next/prev track
|
// starts to play the newly entered next/prev track
|
||||||
startPlaying();
|
startPlaying();
|
||||||
}
|
}
|
||||||
@ -202,6 +211,7 @@ class Playback extends ChangeNotifier {
|
|||||||
.then((value) async {
|
.then((value) async {
|
||||||
_currentTrack = track;
|
_currentTrack = track;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
});
|
});
|
||||||
// await player.play();
|
// await player.play();
|
||||||
return;
|
return;
|
||||||
@ -215,6 +225,7 @@ class Playback extends ChangeNotifier {
|
|||||||
audioQuality: preferences.audioQuality,
|
audioQuality: preferences.audioQuality,
|
||||||
);
|
);
|
||||||
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
|
||||||
|
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
|
||||||
_currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri));
|
_currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri));
|
||||||
await player.core
|
await player.core
|
||||||
.setAudioSource(
|
.setAudioSource(
|
||||||
@ -224,6 +235,7 @@ class Playback extends ChangeNotifier {
|
|||||||
.then((value) {
|
.then((value) {
|
||||||
_currentTrack = spotubeTrack;
|
_currentTrack = spotubeTrack;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
updatePersistence();
|
||||||
});
|
});
|
||||||
// await player.play();
|
// await player.play();
|
||||||
}
|
}
|
||||||
@ -246,6 +258,36 @@ class Playback extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<void> loadFromLocal(Map<String, dynamic> map) {
|
||||||
|
if (map["currentPlaylist"] != null) {
|
||||||
|
_currentPlaylist =
|
||||||
|
CurrentPlaylist.fromJson(jsonDecode(map["currentPlaylist"]));
|
||||||
|
}
|
||||||
|
if (map["currentTrack"] != null) {
|
||||||
|
_currentTrack = Track.fromJson(jsonDecode(map["currentTrack"]));
|
||||||
|
startPlaying().then((_) {
|
||||||
|
Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
||||||
|
if (player.core.playing) {
|
||||||
|
player.pause();
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Map<String, dynamic>> toMap() {
|
||||||
|
return {
|
||||||
|
"currentPlaylist": currentPlaylist != null
|
||||||
|
? jsonEncode(currentPlaylist?.toJson())
|
||||||
|
: null,
|
||||||
|
"currentTrack":
|
||||||
|
currentTrack != null ? jsonEncode(currentTrack?.toJson()) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
final playbackProvider = ChangeNotifierProvider<Playback>((ref) {
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotube/helpers/getLyrics.dart';
|
import 'package:spotube/helpers/getLyrics.dart';
|
||||||
|
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||||
import 'package:spotube/helpers/timed-lyrics.dart';
|
import 'package:spotube/helpers/timed-lyrics.dart';
|
||||||
import 'package:spotube/models/SpotubeTrack.dart';
|
import 'package:spotube/models/SpotubeTrack.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.dart';
|
||||||
@ -118,9 +119,18 @@ final albumTracksQuery = FutureProvider.family<List<TrackSimple>, String>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final currentUserQuery = FutureProvider<User>(
|
final currentUserQuery = FutureProvider<User>(
|
||||||
(ref) {
|
(ref) async {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
return spotify.me.get();
|
final me = await spotify.me.get();
|
||||||
|
if (me.images == null || me.images?.isEmpty == true) {
|
||||||
|
me.images = [
|
||||||
|
Image()
|
||||||
|
..height = 50
|
||||||
|
..width = 50
|
||||||
|
..url = imageToUrlString(me.images),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return me;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
ThemeData darkTheme({
|
ThemeData darkTheme({
|
||||||
required MaterialColor accentMaterialColor,
|
required MaterialColor accentMaterialColor,
|
||||||
@ -7,6 +8,12 @@ ThemeData darkTheme({
|
|||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
extensions: [
|
||||||
|
ShimmerColorTheme(
|
||||||
|
shimmerBackgroundColor: backgroundMaterialColor[700],
|
||||||
|
shimmerColor: backgroundMaterialColor[800],
|
||||||
|
)
|
||||||
|
],
|
||||||
primaryColor: accentMaterialColor,
|
primaryColor: accentMaterialColor,
|
||||||
primarySwatch: accentMaterialColor,
|
primarySwatch: accentMaterialColor,
|
||||||
backgroundColor: backgroundMaterialColor[900],
|
backgroundColor: backgroundMaterialColor[900],
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:spotube/extensions/ShimmerColorTheme.dart';
|
||||||
|
|
||||||
final materialWhite = MaterialColor(Colors.white.value, {
|
final materialWhite = MaterialColor(Colors.white.value, {
|
||||||
50: Colors.white,
|
50: Colors.white,
|
||||||
100: Colors.blueGrey[50]!,
|
100: Colors.blueGrey[50]!,
|
||||||
200: Colors.white,
|
200: Colors.white,
|
||||||
300: Colors.white,
|
300: Colors.white,
|
||||||
400: Colors.white,
|
400: Colors.blueGrey[300]!,
|
||||||
500: Colors.blueGrey,
|
500: Colors.blueGrey,
|
||||||
600: Colors.white,
|
600: Colors.white,
|
||||||
700: Colors.white,
|
700: Colors.grey[700]!,
|
||||||
800: Colors.white,
|
800: Colors.white,
|
||||||
900: Colors.white,
|
900: Colors.white,
|
||||||
});
|
});
|
||||||
@ -19,6 +20,12 @@ ThemeData lightTheme({
|
|||||||
}) {
|
}) {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
|
extensions: [
|
||||||
|
ShimmerColorTheme(
|
||||||
|
shimmerBackgroundColor: backgroundMaterialColor[200],
|
||||||
|
shimmerColor: backgroundMaterialColor[300],
|
||||||
|
)
|
||||||
|
],
|
||||||
primaryColor: accentMaterialColor,
|
primaryColor: accentMaterialColor,
|
||||||
primarySwatch: accentMaterialColor,
|
primarySwatch: accentMaterialColor,
|
||||||
buttonTheme: ButtonThemeData(
|
buttonTheme: ButtonThemeData(
|
||||||
|
@ -37,7 +37,7 @@ abstract class PersistedChangeNotifier extends ChangeNotifier {
|
|||||||
|
|
||||||
FutureOr<Map<String, dynamic>> toMap();
|
FutureOr<Map<String, dynamic>> toMap();
|
||||||
|
|
||||||
Future<void> updatePersistence() async {
|
Future<void> updatePersistence({bool clearNullEntries = false}) async {
|
||||||
for (final entry in (await toMap()).entries) {
|
for (final entry in (await toMap()).entries) {
|
||||||
if (entry.value is bool) {
|
if (entry.value is bool) {
|
||||||
await _localStorage.setBool(entry.key, entry.value);
|
await _localStorage.setBool(entry.key, entry.value);
|
||||||
@ -47,6 +47,8 @@ abstract class PersistedChangeNotifier extends ChangeNotifier {
|
|||||||
await _localStorage.setDouble(entry.key, entry.value);
|
await _localStorage.setDouble(entry.key, entry.value);
|
||||||
} else if (entry.value is String) {
|
} else if (entry.value is String) {
|
||||||
await _localStorage.setString(entry.key, entry.value);
|
await _localStorage.setString(entry.key, entry.value);
|
||||||
|
} else if (entry.value == null && clearNullEntries) {
|
||||||
|
_localStorage.remove(entry.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -702,6 +702,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
skeleton_text:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: skeleton_text
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 2.2.0+10
|
version: 2.2.1+11
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
@ -61,6 +61,7 @@ dependencies:
|
|||||||
version: ^2.0.0
|
version: ^2.0.0
|
||||||
audio_service: ^0.18.4
|
audio_service: ^0.18.4
|
||||||
hookified_infinite_scroll_pagination: ^0.1.0
|
hookified_infinite_scroll_pagination: ^0.1.0
|
||||||
|
skeleton_text: ^3.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user