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:
Kingkor Roy Tirtho 2022-03-30 20:38:14 +06:00
parent 1d09eb451e
commit c27b497c4b
18 changed files with 253 additions and 126 deletions

View File

@ -114,11 +114,31 @@ Do the following:
- Install Development dependencies in linux - Install Development dependencies in linux
- `libwebkit2gtk-4.0-dev`, `libkeybinder-3.0-0` & `libkeybinder-3.0-0-dev` (for Debian/Ubuntu) - `libwebkit2gtk-4.0-dev`, `libkeybinder-3.0-0` & `libkeybinder-3.0-0-dev` (for Debian/Ubuntu)
- `webkit2gtk` & `libkeybinder3` (for Arch/Manjaro) - `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 Do debugging/testing/build etc then submit to us with PR against the development branch (master) & we'll review your code
$ 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

View File

@ -154,38 +154,8 @@ Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube
# Building from source # Building from source
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support You can find the details [here](CONTRIBUTION.md#your-first-code-contribution)
- 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)>
```
# Things that don't work # Things that don't work
- Shows & Podcasts aren't supported as it'd require premium anyway - Shows & Podcasts aren't supported as it'd require premium anyway

View File

@ -2,40 +2,32 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:dotenv/dotenv.dart';
void main(List<String> args) async { void main(List<String> args) async {
String lyricSecret; List<String> val;
String spotifySecret; List<Map> val2;
final cwd = Directory.current.path;
final binSafe = cwd.endsWith("/bin") ? ".." : "";
if (args.isEmpty) { if (args.isEmpty) {
throw ArgumentError("Expected 2 arguments but passed none"); throw ArgumentError("Expected 1-2 arguments but passed none");
} }
if (args.contains("--loadEnv")) { if (args.contains("--local")) {
load(); final secretFilePath = path.join(cwd, binSafe, "secrets.json");
if (env["SPOTIFY_SECRET"] == null || env["LYRICS_SECRET"] == null) { final file = File(secretFilePath);
throw Exception( if (!file.existsSync()) throw Exception("secrets.json file not found");
"'LYRICS_SECRET' or 'SPOTIFY_SECRET' environmental variable aren't set correctly "); final data = jsonDecode(await file.readAsString());
} val = List.castFrom<dynamic, String>(data["LYRICS_SECRET"]);
lyricSecret = env["LYRICS_SECRET"]!; val2 = List.castFrom<dynamic, Map>(data["SPOTIFY_SECRET"]);
spotifySecret = env["SPOTIFY_SECRET"]!;
} else { } else {
lyricSecret = args.first; final decodedLyricSecret = utf8.decode(base64Decode(args.first));
spotifySecret = args.last; 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)); await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart"))
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"))
.writeAsString( .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)};",
); );
} }

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:spotify/spotify.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/PlaybuttonCard.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.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';
import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/helpers/simple-track-to-track.dart';

View File

@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; 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:spotify/spotify.dart';
import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.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/hooks/useForceUpdate.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class AlbumView extends ConsumerWidget { class AlbumView extends HookConsumerWidget {
final AlbumSimple album; final AlbumSimple album;
const AlbumView(this.album, {Key? key}) : super(key: key); const AlbumView(this.album, {Key? key}) : super(key: key);
@ -36,8 +39,12 @@ class AlbumView extends ConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; final isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
SpotifyApi spotify = ref.watch(spotifyProvider); final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider);
final update = useForceUpdate();
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: FutureBuilder<Iterable<TrackSimple>>( body: FutureBuilder<Iterable<TrackSimple>>(
@ -55,10 +62,25 @@ class AlbumView extends ConsumerWidget {
// nav back // nav back
const BackButton(), const BackButton(),
// heart playlist // heart playlist
IconButton( if (auth.isLoggedIn)
icon: const Icon(Icons.favorite_outline_rounded), FutureBuilder<List<bool>>(
onPressed: () {}, 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 // play playlist
IconButton( IconButton(
icon: Icon( icon: Icon(

View File

@ -14,12 +14,15 @@ 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/Logger.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class ArtistProfile extends HookConsumerWidget { class ArtistProfile extends HookConsumerWidget {
final String artistId; 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 @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -44,6 +47,7 @@ class ArtistProfile extends HookConsumerWidget {
); );
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
final update = useForceUpdate();
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
@ -106,19 +110,42 @@ class ArtistProfile extends HookConsumerWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// TODO: Implement check if user follows this artist FutureBuilder<List<bool>>(
// LIMITATION: spotify-dart lib future: spotify.me.isFollowing(
FutureBuilder( FollowingType.artist,
future: Future.value(true), [artistId],
),
builder: (context, snapshot) { builder: (context, snapshot) {
final isFollowing =
snapshot.data?.first == true;
return OutlinedButton( return OutlinedButton(
onPressed: () async { onPressed: () async {
// TODO: make `follow/unfollow` artists button work try {
// LIMITATION: spotify-dart lib 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" ? "Following"
: "Follow"), : "Follow")
: const CircularProgressIndicator
.adaptive(),
); );
}), }),
IconButton( IconButton(

View File

@ -29,6 +29,8 @@ import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
List<String> spotifyScopes = [ List<String> spotifyScopes = [
"playlist-modify-public",
"playlist-modify-private",
"user-library-read", "user-library-read",
"user-library-modify", "user-library-modify",
"user-read-private", "user-read-private",

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

View File

@ -1,5 +1,6 @@
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/Library/UserAlbums.dart';
import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart'; import 'package:spotube/components/Shared/AnonymousFallback.dart';
@ -29,7 +30,7 @@ class UserLibrary extends ConsumerWidget {
? const TabBarView(children: [ ? const TabBarView(children: [
UserPlaylists(), UserPlaylists(),
UserArtists(), UserArtists(),
Icon(Icons.ac_unit_outlined), UserAlbums(),
]) ])
: const AnonymousFallback(), : const AnonymousFallback(),
), ),

View File

@ -162,7 +162,7 @@ class Player extends HookConsumerWidget {
}, },
), ),
), ),
const PlayerActions() PlayerActions()
], ],
), ),
) )

View File

@ -2,22 +2,27 @@ import 'package:flutter/material.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/DownloadTrackButton.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/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlayerActions extends HookConsumerWidget { class PlayerActions extends HookConsumerWidget {
final MainAxisAlignment mainAxisAlignment; final MainAxisAlignment mainAxisAlignment;
const PlayerActions({ PlayerActions({
this.mainAxisAlignment = MainAxisAlignment.center, this.mainAxisAlignment = MainAxisAlignment.center,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final logger = createLogger(PlayerActions);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final SpotifyApi spotifyApi = ref.watch(spotifyProvider); final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
final update = useForceUpdate();
return Row( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ children: [
@ -32,17 +37,20 @@ class PlayerActions extends HookConsumerWidget {
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
bool isLiked = snapshot.data ?? false; bool isLiked = snapshot.data ?? false;
return IconButton( return HeartButton(
icon: Icon( isLiked: isLiked,
!isLiked onPressed: () async {
? Icons.favorite_outline_rounded try {
: Icons.favorite_rounded, if (playback.currentTrack?.id == null) return;
color: isLiked ? Colors.green : null, isLiked
), ? await spotifyApi.tracks.me
onPressed: () { .removeOne(playback.currentTrack!.id!)
if (!isLiked && playback.currentTrack?.id != null) { : await spotifyApi.tracks.me
spotifyApi.tracks.me
.saveOne(playback.currentTrack!.id!); .saveOne(playback.currentTrack!.id!);
} catch (e, stack) {
logger.e("FavoriteButton.onPressed", e, stack);
} finally {
update();
} }
}); });
}), }),

View File

@ -95,7 +95,7 @@ class PlayerView extends HookConsumerWidget {
); );
}), }),
const Spacer(), const Spacer(),
const PlayerActions( PlayerActions(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
), ),
PlayerControls(iconColor: paletteColor.bodyTextColor), PlayerControls(iconColor: paletteColor.bodyTextColor),

View File

@ -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/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.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/useForceUpdate.dart';
import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistView extends ConsumerWidget { class PlaylistView extends HookConsumerWidget {
final logger = createLogger(PlaylistView);
final PlaylistSimple playlist; 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, playPlaylist(Playback playback, List<Track> tracks,
{Track? currentTrack}) async { {Track? currentTrack}) async {
@ -37,15 +41,17 @@ class PlaylistView extends ConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider); final Auth auth = ref.watch(authProvider);
SpotifyApi spotifyApi = ref.watch(spotifyProvider); SpotifyApi spotify = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null && final isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == playlist.id; playback.currentPlaylist?.id == playlist.id;
final update = useForceUpdate();
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: FutureBuilder<Iterable<Track>>( body: FutureBuilder<Iterable<Track>>(
future: playlist.id != "user-liked-tracks" future: playlist.id != "user-liked-tracks"
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all() ? spotify.playlists.getTracksByPlaylistId(playlist.id).all()
: spotifyApi.tracks.me.saved : spotify.tracks.me.saved
.all() .all()
.then((tracks) => tracks.map((e) => e.track!)), .then((tracks) => tracks.map((e) => e.track!)),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -59,10 +65,32 @@ class PlaylistView extends ConsumerWidget {
const BackButton(), const BackButton(),
// heart playlist // heart playlist
if (auth.isLoggedIn) if (auth.isLoggedIn)
IconButton( FutureBuilder<List<bool>>(
icon: const Icon(Icons.favorite_outline_rounded), future: spotify.me.get().then(
onPressed: () {}, (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 // play playlist
IconButton( IconButton(
icon: Icon( icon: Icon(

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

View File

@ -1,6 +1,6 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
simpleAlbumToAlbum(AlbumSimple albumSimple) { Album simpleAlbumToAlbum(AlbumSimple albumSimple) {
Album album = Album(); Album album = Album();
album.albumType = albumSimple.albumType; album.albumType = albumSimple.albumType;
album.artists = albumSimple.artists; album.artists = albumSimple.artists;

View File

@ -0,0 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.dart';
void Function() useForceUpdate() {
final state = useState(null);
return () => state.notifyListeners();
}

View File

@ -169,13 +169,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
dotenv:
dependency: "direct dev"
description:
name: dotenv
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -668,10 +661,12 @@ packages:
spotify: spotify:
dependency: "direct main" dependency: "direct main"
description: description:
name: spotify path: "."
url: "https://pub.dartlang.org" ref: HEAD
source: hosted resolved-ref: e36eb5884de44d39b310d0878779a697048061bd
version: "0.6.0" url: "https://github.com/KRTirtho/spotify-dart.git"
source: git
version: "0.7.0"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,7 +37,8 @@ dependencies:
html: ^0.15.0 html: ^0.15.0
http: ^0.13.4 http: ^0.13.4
shared_preferences: ^2.0.11 shared_preferences: ^2.0.11
spotify: ^0.6.0 spotify:
git: https://github.com/KRTirtho/spotify-dart.git
url_launcher: ^6.0.17 url_launcher: ^6.0.17
youtube_explode_dart: ^1.10.8 youtube_explode_dart: ^1.10.8
infinite_scroll_pagination: ^3.1.0 infinite_scroll_pagination: ^3.1.0
@ -70,7 +71,6 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0
flutter_launcher_icons: ^0.9.2 flutter_launcher_icons: ^0.9.2
dotenv: ^3.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec