diff --git a/README.md b/README.md index 4cfb7966..768c5c4c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Following are the features that currently spotube offers: - Small size & less data hungry - No spotify or youtube ads since it uses all public & free APIs (But it's recommended to support the creators by watching/liking/subscribing to the artists youtube channel or add as favourite track in spotify. Mostly buying spotify premium is the best way to support their valuable creations) - Lyrics -- Downloadable track (WIP) +- Downloadable track + +Spotube - A lightweight+free Spotify desktop-client made with flutter | Product Hunt # Installation @@ -98,9 +100,9 @@ Also, you need a [genius](https://genius.com) account for **lyrics** & a API Cli # TODO: -- [ ] Compile, Debug & Build for **MacOS** +- [x] Compile, Debug & Build for **MacOS** - [x] Add support for show Lyric of currently playing track -- [ ] Track download +- [x] Track download - [ ] Support for playing/streaming podcasts/shows - [ ] Artist, User & Album pages diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index e07e7277..0f5099e8 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Artist/ArtistProfile.dart'; class ArtistCard extends StatelessWidget { final Artist artist; @@ -9,7 +10,13 @@ class ArtistCard extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () {}, + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) { + return ArtistProfile(artist.id!); + }, + )); + }, borderRadius: BorderRadius.circular(10), child: Ink( width: 200, @@ -35,7 +42,7 @@ class ArtistCard extends StatelessWidget { .images?.isNotEmpty ?? false) ? artist.images!.first.url! - : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.svg?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"), + : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"), ), Text( artist.name!, diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart new file mode 100644 index 00000000..da55a726 --- /dev/null +++ b/lib/components/Artist/ArtistProfile.dart @@ -0,0 +1,124 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/helpers/readable-number.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; + +class ArtistProfile extends StatefulWidget { + final String artistId; + const ArtistProfile(this.artistId, {Key? key}) : super(key: key); + + @override + _ArtistProfileState createState() => _ArtistProfileState(); +} + +class _ArtistProfileState extends State { + @override + Widget build(BuildContext context) { + SpotifyApi spotify = context.watch().spotifyApi; + return Scaffold( + body: FutureBuilder( + future: spotify.artists.get(widget.artistId), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + return Column( + children: [ + const PageWindowTitleBar( + leading: BackButton(), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + const SizedBox(width: 50), + CircleAvatar( + maxRadius: 250, + minRadius: 100, + backgroundImage: CachedNetworkImageProvider( + snapshot.data!.images!.first.url!, + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(50)), + child: Text(snapshot.data!.type!.toUpperCase(), + style: Theme.of(context) + .textTheme + .headline6 + ?.copyWith(color: Colors.white)), + ), + Text( + snapshot.data!.name!, + style: Theme.of(context).textTheme.headline2, + ), + Text( + "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 20), + Row( + children: [ + // TODO: Implement check if user follows this artist + // LIMITATION: spotify-dart lib + FutureBuilder( + future: Future.value(true), + builder: (context, snapshot) { + return OutlinedButton( + onPressed: () async { + // TODO: make `follow/unfollow` artists button work + // LIMITATION: spotify-dart lib + }, + child: Text(snapshot.data == true + ? "Following" + : "Follow"), + ); + }), + IconButton( + icon: const Icon(Icons.share_rounded), + onPressed: () { + Clipboard.setData( + ClipboardData( + text: snapshot + .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, + ), + ), + ); + }); + }, + ) + ], + ) + ], + ), + ), + ], + ), + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 78571eb8..cbe86799 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -48,30 +47,6 @@ class _PlayerState extends State with WidgetsBindingObserver { HotKey(KeyCode.space, scope: HotKeyScope.inapp), _playOrPause, ), - // causaes crash in Windows and macOS for aquiring global hotkey of - // keyboard media buttons - if (!Platform.isWindows && !Platform.isMacOS) ...[ - GlobalKeyActions( - HotKey(KeyCode.mediaPlayPause), - _playOrPause, - ), - GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async { - _movePlaylistPositionBy(1); - }), - GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async { - _movePlaylistPositionBy(-1); - }), - GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async { - Playback playback = context.read(); - setState(() { - _isPlaying = false; - _currentTrackId = null; - _duration = null; - _shuffled = false; - }); - playback.reset(); - }) - ] ]; WidgetsBinding.instance?.addObserver(this); WidgetsBinding.instance?.addPostFrameCallback(_init); diff --git a/lib/helpers/readable-number.dart b/lib/helpers/readable-number.dart new file mode 100644 index 00000000..05a169b7 --- /dev/null +++ b/lib/helpers/readable-number.dart @@ -0,0 +1,13 @@ +String toReadableNumber(double num) { + if (num > 999 && num < 99999) { + return "${(num / 1000).toStringAsFixed(1)}K"; + } else if (num > 99999 && num < 999999) { + return "${(num / 1000).toStringAsFixed(0)}K"; + } else if (num > 999999 && num < 999999999) { + return "${(num / 1000000).toStringAsFixed(1)}M"; + } else if (num > 999999999) { + return "${(num / 1000000000).toStringAsFixed(1)}B"; + } else { + return num.toString(); + } +}