From ef121c361386b81bce2ca06d2d6f7a76c2e00f0a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 23 Jan 2022 19:44:26 +0600 Subject: [PATCH] PageWindowTitlebar now compitable with scaffold's appBar AlbumCard, ArtistCard, ArtistAlbumCard & ArtistProfile added UserArtist finished macos build artifacts upload path corrected --- .github/workflows/flutter-build.yml | 10 +- lib/components/Album/AlbumCard.dart | 27 ++ lib/components/Artist/ArtistAlbumView.dart | 92 +++++++ lib/components/Artist/ArtistProfile.dart | 232 ++++++++++++------ lib/components/Library/UserArtists.dart | 20 +- lib/components/Login.dart | 110 ++++----- lib/components/Lyrics.dart | 5 +- lib/components/Player/Player.dart | 4 +- lib/components/Playlist/PlaylistCard.dart | 129 +++------- .../Playlist/PlaylistGenreView.dart | 26 +- lib/components/Settings.dart | 197 ++++++++------- .../Shared/DownloadTrackButton.dart | 2 +- lib/components/Shared/PageWindowTitleBar.dart | 6 +- lib/components/Shared/PlaybuttonCard.dart | 110 +++++++++ lib/helpers/artist-to-string.dart | 2 +- 15 files changed, 598 insertions(+), 374 deletions(-) create mode 100644 lib/components/Album/AlbumCard.dart create mode 100644 lib/components/Artist/ArtistAlbumView.dart create mode 100644 lib/components/Shared/PlaybuttonCard.dart diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 88ab1faa..91d11da6 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -60,12 +60,8 @@ jobs: with: cache: true - run: flutter config --enable-macos-desktop - - run: flutter create . - - run: flutter pub get + - run: flutter build macos - uses: actions/upload-artifact@v2 with: - name: Macos Source Code - path: ./ - - run: flutter build macos - - run: brew install tree - - run: tree ./ + name: Spotube-Macos-Bundle + path: build/macos/Build/Release/Products/spotube.app diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart new file mode 100644 index 00000000..62322289 --- /dev/null +++ b/lib/components/Album/AlbumCard.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Shared/PlaybuttonCard.dart'; +import 'package:spotube/helpers/artist-to-string.dart'; +import 'package:spotube/provider/Playback.dart'; + +class AlbumCard extends StatelessWidget { + final Album album; + const AlbumCard(this.album, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Playback playback = context.watch(); + + return PlaybuttonCard( + imageUrl: album.images!.first.url!, + isPlaying: playback.currentPlaylist?.id != null && + playback.currentPlaylist?.id == album.id, + title: album.name!, + description: + "Alubm • ${artistsToString(album.artists ?? [])}", + onTap: () {}, + onPlaybuttonPressed: () => {}, + ); + } +} diff --git a/lib/components/Artist/ArtistAlbumView.dart b/lib/components/Artist/ArtistAlbumView.dart new file mode 100644 index 00000000..9f0906fd --- /dev/null +++ b/lib/components/Artist/ArtistAlbumView.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart' hide Page; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:provider/provider.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Album/AlbumCard.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; + +class ArtistAlbumView extends StatefulWidget { + final String artistId; + final String artistName; + const ArtistAlbumView( + this.artistId, + this.artistName, { + Key? key, + }) : super(key: key); + + @override + State createState() => _ArtistAlbumViewState(); +} + +class _ArtistAlbumViewState extends State { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void initState() { + super.initState(); + _pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + _fetchPage(int pageKey) async { + try { + SpotifyDI data = context.read(); + Page albums = await data.spotifyApi.artists + .albums(widget.artistId) + .getPage(8, pageKey); + + var items = albums.items!.toList(); + + if (albums.isLast && albums.items != null) { + _pagingController.appendLastPage(items); + } else if (albums.items != null) { + _pagingController.appendPage(items, albums.nextOffset); + } + } catch (e, stack) { + print("[ArtistAlbumView._fetchPage] $e"); + print(stack); + _pagingController.error = e; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const PageWindowTitleBar(leading: BackButton()), + body: Column( + children: [ + Text( + widget.artistName, + style: Theme.of(context).textTheme.headline4, + ), + Expanded( + child: PagedGridView( + pagingController: _pagingController, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 260, + childAspectRatio: 9 / 13, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return AlbumCard(item); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index da55a726..86f9115a 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Album/AlbumCard.dart'; +import 'package:spotube/components/Artist/ArtistAlbumView.dart'; +import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/helpers/readable-number.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -20,102 +23,175 @@ class _ArtistProfileState extends State { Widget build(BuildContext context) { SpotifyApi spotify = context.watch().spotifyApi; return Scaffold( + appBar: const PageWindowTitleBar( + leading: BackButton(), + ), 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( + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ const SizedBox(width: 50), CircleAvatar( - maxRadius: 250, - minRadius: 100, + radius: MediaQuery.of(context).size.width * 0.18, 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, + Flexible( + child: 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, + ), ), - ), - ); - }); - }, - ) - ], - ) - ], + ); + }); + }, + ) + ], + ) + ], + ), ), ), ], ), - ) - ], + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Albums", + style: Theme.of(context).textTheme.headline4, + ), + TextButton( + child: const Text("See All"), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ArtistAlbumView( + widget.artistId, + snapshot.data?.name ?? "KRTX", + ), + )); + }, + ) + ], + ), + const SizedBox(height: 10), + FutureBuilder>( + future: spotify.artists + .albums(snapshot.data!.id!) + .getPage(5, 0) + .then((al) => al.items?.toList() ?? []), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive()); + } + return Center( + child: Wrap( + spacing: 20, + runSpacing: 20, + children: snapshot.data + ?.map((album) => AlbumCard(album)) + .toList() ?? + [], + ), + ); + }, + ), + const SizedBox(height: 20), + Text( + "Fans also likes", + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox(height: 10), + FutureBuilder>( + future: spotify.artists.getRelatedArtists(widget.artistId), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive()); + } + + return Center( + child: Wrap( + spacing: 20, + runSpacing: 20, + children: snapshot.data + ?.map((artist) => ArtistCard(artist)) + .toList() ?? + [], + ), + ); + }, + ) + ], + ), ); }, ), diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index b1ac2679..60291c33 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -13,8 +13,8 @@ class UserArtists extends StatefulWidget { } class _UserArtistsState extends State { - final PagingController _pagingController = - PagingController(firstPageKey: 0); + final PagingController _pagingController = + PagingController(firstPageKey: ""); @override void initState() { @@ -23,22 +23,16 @@ class _UserArtistsState extends State { _pagingController.addPageRequestListener((pageKey) async { try { SpotifyDI data = context.read(); - var offset = - _pagingController.value.itemList?.elementAt(pageKey).id ?? ""; CursorPage artists = await data.spotifyApi.me .following(FollowingType.artist) - .getPage(15, offset); + .getPage(15, pageKey); var items = artists.items!.toList(); if (artists.items != null && items.length < 15) { _pagingController.appendLastPage(items); } else if (artists.items != null) { - var yetToBe = [ - ...(_pagingController.value.itemList ?? []), - ...items - ]; - _pagingController.appendPage(items, yetToBe.length - 1); + _pagingController.appendPage(items, items.last.id); } } catch (e, stack) { _pagingController.error = e; @@ -49,6 +43,12 @@ class _UserArtistsState extends State { }); } + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { SpotifyDI data = context.watch(); diff --git a/lib/components/Login.dart b/lib/components/Login.dart index 5c59bab9..aad4cc44 100644 --- a/lib/components/Login.dart +++ b/lib/components/Login.dart @@ -34,75 +34,69 @@ class _LoginState extends State { return Consumer( builder: (context, authState, child) { return Scaffold( - body: Column( - children: [ - const PageWindowTitleBar(), - Expanded( - child: Center( + appBar: const PageWindowTitleBar(), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + "assets/spotube-logo.png", + width: 400, + height: 400, + ), + Text("Add your spotify credentials to get started", + style: Theme.of(context).textTheme.headline4), + const Text( + "Don't worry, any of your credentials won't be collected or shared with anyone"), + const SizedBox( + height: 10, + ), + Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, children: [ - Image.asset( - "assets/spotube-logo.png", - width: 400, - height: 400, + TextField( + decoration: const InputDecoration( + hintText: "Spotify Client ID", + label: Text("ClientID"), + ), + onChanged: (value) { + setState(() { + clientId = value; + }); + }, ), - Text("Add your spotify credentials to get started", - style: Theme.of(context).textTheme.headline4), - const Text( - "Don't worry, any of your credentials won't be collected or shared with anyone"), const SizedBox( height: 10, ), - Container( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - children: [ - TextField( - decoration: const InputDecoration( - hintText: "Spotify Client ID", - label: Text("ClientID"), - ), - onChanged: (value) { - setState(() { - clientId = value; - }); - }, - ), - const SizedBox( - height: 10, - ), - TextField( - decoration: const InputDecoration( - hintText: "Spotify Client Secret", - label: Text("Client Secret"), - ), - onChanged: (value) { - setState(() { - clientSecret = value; - }); - }, - ), - const SizedBox( - height: 10, - ), - ElevatedButton( - onPressed: () { - handleLogin(authState); - }, - child: const Text("Submit"), - ) - ], + TextField( + decoration: const InputDecoration( + hintText: "Spotify Client Secret", + label: Text("Client Secret"), ), + onChanged: (value) { + setState(() { + clientSecret = value; + }); + }, ), + const SizedBox( + height: 10, + ), + ElevatedButton( + onPressed: () { + handleLogin(authState); + }, + child: const Text("Submit"), + ) ], ), ), - ), - ], + ], + ), ), ); }, diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 05827d49..dcc5b19a 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/components/Settings.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/getLyrics.dart'; @@ -29,7 +30,7 @@ class _LyricsState extends State { playback.currentTrack!.id != _lyrics["id"]) { getLyrics( playback.currentTrack!.name!, - artistsToString(playback.currentTrack!.artists ?? []), + artistsToString(playback.currentTrack!.artists ?? []), apiKey: userPreferences.geniusAccessToken!, optimizeQuery: true, ).then((lyrics) { @@ -90,7 +91,7 @@ class _LyricsState extends State { ), Center( child: Text( - artistsToString(playback.currentTrack?.artists ?? []), + artistsToString(playback.currentTrack?.artists ?? []), style: Theme.of(context).textTheme.headline5, ), ), diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index cbe86799..e4d4e345 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -240,8 +240,8 @@ class _PlayerState extends State with WidgetsBindingObserver { playback.currentTrack?.name ?? "Not playing", style: const TextStyle(fontWeight: FontWeight.bold), ), - Text( - artistsToString(playback.currentTrack?.artists ?? [])) + Text(artistsToString( + playback.currentTrack?.artists ?? [])) ], ), ), diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index c024da87..28819e9a 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; +import 'package:spotube/components/Shared/PlaybuttonCard.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -16,7 +16,13 @@ class PlaylistCard extends StatefulWidget { class _PlaylistCardState extends State { @override Widget build(BuildContext context) { - return InkWell( + Playback playback = context.watch(); + bool isPlaylistPlaying = playback.currentPlaylist != null && + playback.currentPlaylist!.id == widget.playlist.id; + return PlaybuttonCard( + title: widget.playlist.name!, + imageUrl: widget.playlist.images![0].url!, + isPlaying: isPlaylistPlaying, onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) { @@ -24,108 +30,29 @@ class _PlaylistCardState extends State { }, )); }, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: Ink( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // thumbnail of the playlist - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: widget.playlist.images![0].url!, - progressIndicatorBuilder: (context, url, progress) { - return CircularProgressIndicator.adaptive( - value: progress.progress, - ); - }, - ), - ), - Positioned.directional( - textDirection: TextDirection.ltr, - bottom: 10, - end: 5, - child: Builder(builder: (context) { - Playback playback = context.watch(); - SpotifyDI data = context.watch(); - bool isPlaylistPlaying = playback.currentPlaylist != - null && - playback.currentPlaylist!.id == widget.playlist.id; - return ElevatedButton( - onPressed: () async { - if (isPlaylistPlaying) return; + onPlaybuttonPressed: () async { + if (isPlaylistPlaying) return; + SpotifyDI data = context.read(); - List tracks = - (widget.playlist.id != "user-liked-tracks" - ? await data.spotifyApi.playlists - .getTracksByPlaylistId( - widget.playlist.id!) - .all() - : await data.spotifyApi.tracks.me.saved - .all() - .then((tracks) => - tracks.map((e) => e.track!))) - .toList(); + List tracks = (widget.playlist.id != "user-liked-tracks" + ? await data.spotifyApi.playlists + .getTracksByPlaylistId(widget.playlist.id!) + .all() + : await data.spotifyApi.tracks.me.saved + .all() + .then((tracks) => tracks.map((e) => e.track!))) + .toList(); - if (tracks.isEmpty) return; + if (tracks.isEmpty) return; - playback.setCurrentPlaylist = CurrentPlaylist( - tracks: tracks, - id: widget.playlist.id!, - name: widget.playlist.name!, - thumbnail: widget.playlist.images!.first.url!, - ); - playback.setCurrentTrack = tracks.first; - }, - child: Icon( - isPlaylistPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), - style: ButtonStyle( - shape: MaterialStateProperty.all( - const CircleBorder(), - ), - padding: MaterialStateProperty.all( - const EdgeInsets.all(16), - ), - ), - ); - }), - ) - ], - ), - const SizedBox(height: 5), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 10), - child: Column( - children: [ - Text( - widget.playlist.name!, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ) - ], - ), - ), - ), + playback.setCurrentPlaylist = CurrentPlaylist( + tracks: tracks, + id: widget.playlist.id!, + name: widget.playlist.name!, + thumbnail: widget.playlist.images!.first.url!, + ); + playback.setCurrentTrack = tracks.first; + }, ); } } diff --git a/lib/components/Playlist/PlaylistGenreView.dart b/lib/components/Playlist/PlaylistGenreView.dart index 15879a9a..44597c60 100644 --- a/lib/components/Playlist/PlaylistGenreView.dart +++ b/lib/components/Playlist/PlaylistGenreView.dart @@ -23,11 +23,11 @@ class _PlaylistGenreViewState extends State { @override Widget build(BuildContext context) { return Scaffold( + appBar: const PageWindowTitleBar( + leading: BackButton(), + ), body: Column( children: [ - const PageWindowTitleBar( - leading: BackButton(), - ), Text( widget.genreName, style: Theme.of(context).textTheme.headline4, @@ -51,15 +51,17 @@ class _PlaylistGenreViewState extends State { if (!snapshot.hasData) { return const CircularProgressIndicator.adaptive(); } - return Wrap( - children: snapshot.data! - .map( - (playlist) => Padding( - padding: const EdgeInsets.all(8.0), - child: PlaylistCard(playlist), - ), - ) - .toList(), + return Center( + child: Wrap( + children: snapshot.data! + .map( + (playlist) => Padding( + padding: const EdgeInsets.all(8.0), + child: PlaylistCard(playlist), + ), + ) + .toList(), + ), ); }), ), diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 348a4ebc..517e0b46 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -40,112 +40,107 @@ class _SettingsState extends State { UserPreferences preferences = context.watch(); return Scaffold( - body: Column( - children: [ - PageWindowTitleBar( - leading: const BackButton(), - center: Text( - "Settings", - style: Theme.of(context).textTheme.headline5, - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( + appBar: PageWindowTitleBar( + leading: const BackButton(), + center: Text( + "Settings", + style: Theme.of(context).textTheme.headline5, + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( children: [ - Row( - children: [ - Expanded( - flex: 2, - child: Text( - "Genius Access Token", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - Expanded( - flex: 1, - child: TextField( - controller: _textEditingController, - decoration: InputDecoration( - hintText: preferences.geniusAccessToken, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - onPressed: _geniusAccessToken != null - ? () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - preferences - .setGeniusAccessToken(_geniusAccessToken); - localStorage.setString( - LocalStorageKeys.geniusAccessToken, - _geniusAccessToken!); - setState(() { - _geniusAccessToken = null; - }); - _textEditingController?.text = ""; - } - : null, - child: const Text("Save"), - ), - ) - ], + Expanded( + flex: 2, + child: Text( + "Genius Access Token", + style: Theme.of(context).textTheme.subtitle1, + ), ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Theme"), - DropdownButton( - value: MyApp.of(context)?.getThemeMode(), - items: const [ - DropdownMenuItem( - child: Text( - "Dark", - ), - value: ThemeMode.dark, - ), - DropdownMenuItem( - child: Text( - "Light", - ), - value: ThemeMode.light, - ), - DropdownMenuItem( - child: Text("System"), - value: ThemeMode.system, - ), - ], - onChanged: (value) { - if (value != null) { - MyApp.of(context)?.setThemeMode(value); - } - }, - ) - ], + Expanded( + flex: 1, + child: TextField( + controller: _textEditingController, + decoration: InputDecoration( + hintText: preferences.geniusAccessToken, + ), + ), ), - const SizedBox(height: 10), - Builder(builder: (context) { - var auth = context.read(); - return ElevatedButton( - child: const Text("Logout"), - onPressed: () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - await localStorage.clear(); - auth.logout(); - Navigator.of(context).pop(); - }, - ); - }) + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: _geniusAccessToken != null + ? () async { + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + preferences + .setGeniusAccessToken(_geniusAccessToken); + localStorage.setString( + LocalStorageKeys.geniusAccessToken, + _geniusAccessToken!); + setState(() { + _geniusAccessToken = null; + }); + _textEditingController?.text = ""; + } + : null, + child: const Text("Save"), + ), + ) ], ), - ), - ], + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Theme"), + DropdownButton( + value: MyApp.of(context)?.getThemeMode(), + items: const [ + DropdownMenuItem( + child: Text( + "Dark", + ), + value: ThemeMode.dark, + ), + DropdownMenuItem( + child: Text( + "Light", + ), + value: ThemeMode.light, + ), + DropdownMenuItem( + child: Text("System"), + value: ThemeMode.system, + ), + ], + onChanged: (value) { + if (value != null) { + MyApp.of(context)?.setThemeMode(value); + } + }, + ) + ], + ), + const SizedBox(height: 10), + Builder(builder: (context) { + var auth = context.read(); + return ElevatedButton( + child: const Text("Logout"), + onPressed: () async { + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + await localStorage.clear(); + auth.logout(); + Navigator.of(context).pop(); + }, + ); + }) + ], + ), ), ); } diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart index 6d66da8a..3d8d9fcc 100644 --- a/lib/components/Shared/DownloadTrackButton.dart +++ b/lib/components/Shared/DownloadTrackButton.dart @@ -70,7 +70,7 @@ class _DownloadTrackButtonState extends State { String downloadFolder = path.join( (await path_provider.getDownloadsDirectory())!.path, "Spotube"); String fileName = - "${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3"; + "${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3"; File outputFile = File(path.join(downloadFolder, fileName)); if (!outputFile.existsSync()) { outputFile.createSync(recursive: true); diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index 97c35880..506998e8 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -43,11 +43,15 @@ class TitleBarActionButtons extends StatelessWidget { } } -class PageWindowTitleBar extends StatelessWidget { +class PageWindowTitleBar extends StatelessWidget + implements PreferredSizeWidget { final Widget? leading; final Widget? center; const PageWindowTitleBar({Key? key, this.leading, this.center}) : super(key: key); + @override + Size get preferredSize => Size.fromHeight(appWindow.titleBarHeight); + @override Widget build(BuildContext context) { return WindowTitleBarBox( diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart new file mode 100644 index 00000000..42b350e0 --- /dev/null +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -0,0 +1,110 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +class PlaybuttonCard extends StatelessWidget { + final void Function()? onTap; + final void Function()? onPlaybuttonPressed; + final String? description; + final String imageUrl; + final bool isPlaying; + final String title; + const PlaybuttonCard({ + required this.imageUrl, + required this.isPlaying, + required this.title, + this.description, + this.onPlaybuttonPressed, + this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Ink( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // thumbnail of the playlist + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: imageUrl, + progressIndicatorBuilder: (context, url, progress) { + return CircularProgressIndicator.adaptive( + value: progress.progress, + ); + }, + ), + ), + Positioned.directional( + textDirection: TextDirection.ltr, + bottom: 10, + end: 5, + child: Builder(builder: (context) { + return ElevatedButton( + onPressed: onPlaybuttonPressed, + child: Icon( + isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + style: ButtonStyle( + shape: MaterialStateProperty.all( + const CircleBorder(), + ), + padding: MaterialStateProperty.all( + const EdgeInsets.all(16), + ), + ), + ); + }), + ) + ], + ), + const SizedBox(height: 5), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Column( + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (description != null) ...[ + const SizedBox(height: 10), + Text( + description!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).textTheme.headline4?.color, + ), + ) + ] + ], + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/helpers/artist-to-string.dart b/lib/helpers/artist-to-string.dart index 36d09b25..6f3b30f0 100644 --- a/lib/helpers/artist-to-string.dart +++ b/lib/helpers/artist-to-string.dart @@ -1,5 +1,5 @@ import 'package:spotify/spotify.dart'; -String artistsToString(List artists) { +String artistsToString(List artists) { return artists.map((e) => e.name?.replaceAll(",", " ")).join(", "); }