mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Follow/Unfollow artist/playlist support
Save/Remove album support Add/Remove track from favorite support Easier way to create secrets locally Updated contribution details according newest changes
This commit is contained in:
parent
1d09eb451e
commit
c27b497c4b
@ -114,11 +114,31 @@ Do the following:
|
||||
- Install Development dependencies in linux
|
||||
- `libwebkit2gtk-4.0-dev`, `libkeybinder-3.0-0` & `libkeybinder-3.0-0-dev` (for Debian/Ubuntu)
|
||||
- `webkit2gtk` & `libkeybinder3` (for Arch/Manjaro)
|
||||
- Clone the Repo
|
||||
- Clone the Repo & Run `flutter pub get` in the Terminal
|
||||
- Create a `secrets.json` in root of the project. The structure should be similar to the following example:
|
||||
```jsoc name="secrets.json"
|
||||
{
|
||||
"LYRICS_SECRET": [
|
||||
"Bo3LQEMcL2xUAJ6yCfQowV6f8K78s9J9FLa67AsyWmvhkP9LWikkgcEyFrzvs7jsR",
|
||||
"HiLHxLj8uv2VhBZfq9BQ9HVrWQk5Jc8aneMZX8RV4KjTmC387K692xrbNK35c8Qe4",
|
||||
],
|
||||
"SPOTIFY_SECRET": [
|
||||
{
|
||||
"clientId": "9ed19daf-c7a2-4c28-91ac-2c5283ad86cf",
|
||||
"clientSecret": "236d5822-820e-457e-b18c-10e258c9386b"
|
||||
},
|
||||
{
|
||||
"clientId": "b4769027-e048-4485-8f0b-b8a336f2cd97",
|
||||
"clientSecret": "41df6ea4-eba2-4d42-b7be-6f727555fccc"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
> You can add more clientId/clientSecret/genius-access-token if you want. The credentials used in the example are dummy (fake). You've to use your own secrets
|
||||
- Finally run these following commands in the root of the project to start the Spotube Locally
|
||||
```bash
|
||||
$ dart create-secrets.dart --local
|
||||
$ flutter run -d <window|macos|linux|(<android-device-id>)>
|
||||
```
|
||||
|
||||
```bash
|
||||
$ flutter pub get
|
||||
$ flutter run -d <window|macos|linux>
|
||||
```
|
||||
|
||||
Do debugging/testing/build etc then submit to us with PR against the development branch (master) & we'll review your code. **DO NOT TOUCH `build` branch** as it is only for CI & releases
|
||||
Do debugging/testing/build etc then submit to us with PR against the development branch (master) & we'll review your code
|
34
README.md
34
README.md
@ -154,38 +154,8 @@ Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube
|
||||
|
||||
# Building from source
|
||||
|
||||
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support
|
||||
- Install Development dependencies in linux
|
||||
- `libwebkit2gtk-4.0-dev`, `libkeybinder-3.0-0` & `libkeybinder-3.0-0-dev` (for Debian/Ubuntu)
|
||||
- `webkit2gtk` & `libkeybinder3` (for Arch/Manjaro)
|
||||
- Clone the Repo & run
|
||||
```bash
|
||||
$ flutter pub get
|
||||
```
|
||||
- Create a `.env` file containing 2 secrets `LYRICS_SECRET` & `SPOTIFY_SECRET`. These secrets should be a **base64 encoded JSON string**
|
||||
- Structure of `LYRICS_SECRET` json string:
|
||||
```jsonc
|
||||
[
|
||||
"<secret genius access tokens>",
|
||||
// and so on...
|
||||
]
|
||||
```
|
||||
- Structure of `SPOTIFY_SECRET` json string:
|
||||
```jsonc
|
||||
[
|
||||
{"clientId": "<Spotify Client Id>", "clientSecret": "<Spotify Client Secret>"},
|
||||
// and so on ....
|
||||
]
|
||||
```
|
||||
> You can base64 encode the JSON [here](https://www.base64encode.org/)
|
||||
- Run following in the terminal to generate secrets for your fork
|
||||
```bash
|
||||
$ dart bin/create-secrets.dart --loadEnv
|
||||
```
|
||||
Finally, to start the app run:
|
||||
```bash
|
||||
$ flutter run -d <window|macos|linux|(android-device-id)>
|
||||
```
|
||||
You can find the details [here](CONTRIBUTION.md#your-first-code-contribution)
|
||||
|
||||
# Things that don't work
|
||||
|
||||
- Shows & Podcasts aren't supported as it'd require premium anyway
|
||||
|
@ -2,40 +2,32 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:dotenv/dotenv.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
String lyricSecret;
|
||||
String spotifySecret;
|
||||
List<String> val;
|
||||
List<Map> val2;
|
||||
|
||||
final cwd = Directory.current.path;
|
||||
final binSafe = cwd.endsWith("/bin") ? ".." : "";
|
||||
if (args.isEmpty) {
|
||||
throw ArgumentError("Expected 2 arguments but passed none");
|
||||
throw ArgumentError("Expected 1-2 arguments but passed none");
|
||||
}
|
||||
if (args.contains("--loadEnv")) {
|
||||
load();
|
||||
if (env["SPOTIFY_SECRET"] == null || env["LYRICS_SECRET"] == null) {
|
||||
throw Exception(
|
||||
"'LYRICS_SECRET' or 'SPOTIFY_SECRET' environmental variable aren't set correctly ");
|
||||
}
|
||||
lyricSecret = env["LYRICS_SECRET"]!;
|
||||
spotifySecret = env["SPOTIFY_SECRET"]!;
|
||||
if (args.contains("--local")) {
|
||||
final secretFilePath = path.join(cwd, binSafe, "secrets.json");
|
||||
final file = File(secretFilePath);
|
||||
if (!file.existsSync()) throw Exception("secrets.json file not found");
|
||||
final data = jsonDecode(await file.readAsString());
|
||||
val = List.castFrom<dynamic, String>(data["LYRICS_SECRET"]);
|
||||
val2 = List.castFrom<dynamic, Map>(data["SPOTIFY_SECRET"]);
|
||||
} else {
|
||||
lyricSecret = args.first;
|
||||
spotifySecret = args.last;
|
||||
final decodedLyricSecret = utf8.decode(base64Decode(args.first));
|
||||
final decodedSpotifySecrete = utf8.decode(base64Decode(args.last));
|
||||
val = List.castFrom<dynamic, String>(jsonDecode(decodedLyricSecret));
|
||||
val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecrete));
|
||||
}
|
||||
|
||||
final decodedLyricSecret = utf8.decode(base64Decode(lyricSecret));
|
||||
final decodedSpotifySecrete = utf8.decode(base64Decode(spotifySecret));
|
||||
final val = jsonDecode(decodedLyricSecret);
|
||||
final val2 = jsonDecode(decodedSpotifySecrete);
|
||||
if (val is! List || (val2 is! List && (val2 as List).first is! Map)) {
|
||||
throw Exception(
|
||||
"'LYRICS_SECRET' and 'SPOTIFY_SECRET' Environmental Variable isn't configured properly");
|
||||
}
|
||||
|
||||
await File(path.join(
|
||||
Directory.current.path, "lib/models/generated_secrets.dart"))
|
||||
await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart"))
|
||||
.writeAsString(
|
||||
"final List<String> lyricsSecrets = $decodedLyricSecret;\nfinal List<Map<String, dynamic>> spotifySecrets = $decodedSpotifySecrete;",
|
||||
"final List<String> lyricsSecrets = ${jsonEncode(val)};\nfinal List<Map<String, dynamic>> spotifySecrets = ${jsonEncode(val2)};",
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumView.dart';
|
||||
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
|
||||
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
|
@ -1,14 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class AlbumView extends ConsumerWidget {
|
||||
class AlbumView extends HookConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
const AlbumView(this.album, {Key? key}) : super(key: key);
|
||||
|
||||
@ -36,8 +39,12 @@ class AlbumView extends ConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
|
||||
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
||||
final SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
|
||||
final update = useForceUpdate();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: FutureBuilder<Iterable<TrackSimple>>(
|
||||
@ -55,10 +62,25 @@ class AlbumView extends ConsumerWidget {
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
IconButton(
|
||||
icon: const Icon(Icons.favorite_outline_rounded),
|
||||
onPressed: () {},
|
||||
),
|
||||
if (auth.isLoggedIn)
|
||||
FutureBuilder<List<bool>>(
|
||||
future: spotify.me.isSavedAlbums([album.id!]),
|
||||
builder: (context, snapshot) {
|
||||
final isSaved = snapshot.data?.first == true;
|
||||
return HeartButton(
|
||||
isLiked: isSaved,
|
||||
onPressed: () {
|
||||
(isSaved
|
||||
? spotify.me.removeAlbums(
|
||||
[album.id!],
|
||||
)
|
||||
: spotify.me.saveAlbums(
|
||||
[album.id!],
|
||||
))
|
||||
.then((_) => update());
|
||||
},
|
||||
);
|
||||
}),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
@ -14,12 +14,15 @@ import 'package:spotube/helpers/readable-number.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/hooks/useBreakpointValue.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class ArtistProfile extends HookConsumerWidget {
|
||||
final String artistId;
|
||||
const ArtistProfile(this.artistId, {Key? key}) : super(key: key);
|
||||
final logger = createLogger(ArtistProfile);
|
||||
ArtistProfile(this.artistId, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -44,6 +47,7 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final breakpoint = useBreakpoints();
|
||||
final update = useForceUpdate();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
@ -106,19 +110,42 @@ class ArtistProfile extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// TODO: Implement check if user follows this artist
|
||||
// LIMITATION: spotify-dart lib
|
||||
FutureBuilder(
|
||||
future: Future.value(true),
|
||||
FutureBuilder<List<bool>>(
|
||||
future: spotify.me.isFollowing(
|
||||
FollowingType.artist,
|
||||
[artistId],
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final isFollowing =
|
||||
snapshot.data?.first == true;
|
||||
return OutlinedButton(
|
||||
onPressed: () async {
|
||||
// TODO: make `follow/unfollow` artists button work
|
||||
// LIMITATION: spotify-dart lib
|
||||
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 {
|
||||
update();
|
||||
}
|
||||
},
|
||||
child: Text(snapshot.data == true
|
||||
child: snapshot.hasData
|
||||
? Text(isFollowing
|
||||
? "Following"
|
||||
: "Follow"),
|
||||
: "Follow")
|
||||
: const CircularProgressIndicator
|
||||
.adaptive(),
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
|
@ -29,6 +29,8 @@ import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
List<String> spotifyScopes = [
|
||||
"playlist-modify-public",
|
||||
"playlist-modify-private",
|
||||
"user-library-read",
|
||||
"user-library-modify",
|
||||
"user-read-private",
|
||||
|
37
lib/components/Library/UserAlbums.dart
Normal file
37
lib/components/Library/UserAlbums.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/helpers/simple-album-to-album.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class UserAlbums extends ConsumerWidget {
|
||||
const UserAlbums({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
|
||||
return FutureBuilder<Iterable<AlbumSimple>>(
|
||||
future: spotifyApi.me.savedAlbums().all(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && snapshot.data == null) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Wrap(
|
||||
spacing: 20, // gap between adjacent chips
|
||||
runSpacing: 20, // gap between lines
|
||||
alignment: WrapAlignment.center,
|
||||
children: snapshot.data!
|
||||
.map((album) => AlbumCard(simpleAlbumToAlbum(album)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/components/Library/UserAlbums.dart';
|
||||
import 'package:spotube/components/Library/UserArtists.dart';
|
||||
import 'package:spotube/components/Library/UserPlaylists.dart';
|
||||
import 'package:spotube/components/Shared/AnonymousFallback.dart';
|
||||
@ -29,7 +30,7 @@ class UserLibrary extends ConsumerWidget {
|
||||
? const TabBarView(children: [
|
||||
UserPlaylists(),
|
||||
UserArtists(),
|
||||
Icon(Icons.ac_unit_outlined),
|
||||
UserAlbums(),
|
||||
])
|
||||
: const AnonymousFallback(),
|
||||
),
|
||||
|
@ -162,7 +162,7 @@ class Player extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
const PlayerActions()
|
||||
PlayerActions()
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -2,22 +2,27 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class PlayerActions extends HookConsumerWidget {
|
||||
final MainAxisAlignment mainAxisAlignment;
|
||||
const PlayerActions({
|
||||
PlayerActions({
|
||||
this.mainAxisAlignment = MainAxisAlignment.center,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final logger = createLogger(PlayerActions);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
final Playback playback = ref.watch(playbackProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
final update = useForceUpdate();
|
||||
return Row(
|
||||
mainAxisAlignment: mainAxisAlignment,
|
||||
children: [
|
||||
@ -32,17 +37,20 @@ class PlayerActions extends HookConsumerWidget {
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
bool isLiked = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
!isLiked
|
||||
? Icons.favorite_outline_rounded
|
||||
: Icons.favorite_rounded,
|
||||
color: isLiked ? Colors.green : null,
|
||||
),
|
||||
onPressed: () {
|
||||
if (!isLiked && playback.currentTrack?.id != null) {
|
||||
spotifyApi.tracks.me
|
||||
return HeartButton(
|
||||
isLiked: isLiked,
|
||||
onPressed: () async {
|
||||
try {
|
||||
if (playback.currentTrack?.id == null) return;
|
||||
isLiked
|
||||
? await spotifyApi.tracks.me
|
||||
.removeOne(playback.currentTrack!.id!)
|
||||
: await spotifyApi.tracks.me
|
||||
.saveOne(playback.currentTrack!.id!);
|
||||
} catch (e, stack) {
|
||||
logger.e("FavoriteButton.onPressed", e, stack);
|
||||
} finally {
|
||||
update();
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
@ -95,7 +95,7 @@ class PlayerView extends HookConsumerWidget {
|
||||
);
|
||||
}),
|
||||
const Spacer(),
|
||||
const PlayerActions(
|
||||
PlayerActions(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
),
|
||||
PlayerControls(iconColor: paletteColor.bodyTextColor),
|
||||
|
@ -1,16 +1,20 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/hooks/useForceUpdate.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class PlaylistView extends ConsumerWidget {
|
||||
class PlaylistView extends HookConsumerWidget {
|
||||
final logger = createLogger(PlaylistView);
|
||||
final PlaylistSimple playlist;
|
||||
const PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
||||
PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
||||
|
||||
playPlaylist(Playback playback, List<Track> tracks,
|
||||
{Track? currentTrack}) async {
|
||||
@ -37,15 +41,17 @@ class PlaylistView extends ConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
final Auth auth = ref.watch(authProvider);
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||
playback.currentPlaylist?.id == playlist.id;
|
||||
final update = useForceUpdate();
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
body: FutureBuilder<Iterable<Track>>(
|
||||
future: playlist.id != "user-liked-tracks"
|
||||
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
|
||||
: spotifyApi.tracks.me.saved
|
||||
? spotify.playlists.getTracksByPlaylistId(playlist.id).all()
|
||||
: spotify.tracks.me.saved
|
||||
.all()
|
||||
.then((tracks) => tracks.map((e) => e.track!)),
|
||||
builder: (context, snapshot) {
|
||||
@ -59,10 +65,32 @@ class PlaylistView extends ConsumerWidget {
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
if (auth.isLoggedIn)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.favorite_outline_rounded),
|
||||
onPressed: () {},
|
||||
FutureBuilder<List<bool>>(
|
||||
future: spotify.me.get().then(
|
||||
(me) => spotify.playlists
|
||||
.followedBy(playlist.id!, [me.id!]),
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
final isFollowing =
|
||||
snapshot.data?.first ?? false;
|
||||
return HeartButton(
|
||||
isLiked: isFollowing,
|
||||
onPressed: () async {
|
||||
try {
|
||||
isFollowing
|
||||
? spotify.playlists
|
||||
.unfollowPlaylist(playlist.id!)
|
||||
: spotify.playlists
|
||||
.followPlaylist(playlist.id!);
|
||||
} catch (e, stack) {
|
||||
logger.e(
|
||||
"FollowButton.onPressed", e, stack);
|
||||
} finally {
|
||||
update();
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
|
22
lib/components/Shared/HeartButton.dart
Normal file
22
lib/components/Shared/HeartButton.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HeartButton extends StatelessWidget {
|
||||
final bool isLiked;
|
||||
final void Function() onPressed;
|
||||
const HeartButton({
|
||||
required this.isLiked,
|
||||
required this.onPressed,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(
|
||||
!isLiked ? Icons.favorite_outline_rounded : Icons.favorite_rounded,
|
||||
color: isLiked ? Colors.green : null,
|
||||
),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
simpleAlbumToAlbum(AlbumSimple albumSimple) {
|
||||
Album simpleAlbumToAlbum(AlbumSimple albumSimple) {
|
||||
Album album = Album();
|
||||
album.albumType = albumSimple.albumType;
|
||||
album.artists = albumSimple.artists;
|
||||
|
6
lib/hooks/useForceUpdate.dart
Normal file
6
lib/hooks/useForceUpdate.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
void Function() useForceUpdate() {
|
||||
final state = useState(null);
|
||||
return () => state.notifyListeners();
|
||||
}
|
17
pubspec.lock
17
pubspec.lock
@ -169,13 +169,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
dotenv:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: dotenv
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -668,10 +661,12 @@ packages:
|
||||
spotify:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: spotify
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: e36eb5884de44d39b310d0878779a697048061bd
|
||||
url: "https://github.com/KRTirtho/spotify-dart.git"
|
||||
source: git
|
||||
version: "0.7.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -37,7 +37,8 @@ dependencies:
|
||||
html: ^0.15.0
|
||||
http: ^0.13.4
|
||||
shared_preferences: ^2.0.11
|
||||
spotify: ^0.6.0
|
||||
spotify:
|
||||
git: https://github.com/KRTirtho/spotify-dart.git
|
||||
url_launcher: ^6.0.17
|
||||
youtube_explode_dart: ^1.10.8
|
||||
infinite_scroll_pagination: ^3.1.0
|
||||
@ -70,7 +71,6 @@ dev_dependencies:
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^1.0.0
|
||||
flutter_launcher_icons: ^0.9.2
|
||||
dotenv: ^3.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
Loading…
Reference in New Issue
Block a user