PageWindowTitlebar now compitable with scaffold's appBar

AlbumCard, ArtistCard, ArtistAlbumCard & ArtistProfile added
UserArtist finished
macos build artifacts upload path corrected
This commit is contained in:
Kingkor Roy Tirtho 2022-01-23 19:44:26 +06:00
parent 46b652788f
commit ef121c3613
15 changed files with 598 additions and 374 deletions

View File

@ -60,12 +60,8 @@ jobs:
with: with:
cache: true cache: true
- run: flutter config --enable-macos-desktop - run: flutter config --enable-macos-desktop
- run: flutter create . - run: flutter build macos
- run: flutter pub get
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: Macos Source Code name: Spotube-Macos-Bundle
path: ./ path: build/macos/Build/Release/Products/spotube.app
- run: flutter build macos
- run: brew install tree
- run: tree ./

View File

@ -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<Playback>();
return PlaybuttonCard(
imageUrl: album.images!.first.url!,
isPlaying: playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == album.id,
title: album.name!,
description:
"Alubm • ${artistsToString<ArtistSimple>(album.artists ?? [])}",
onTap: () {},
onPlaybuttonPressed: () => {},
);
}
}

View File

@ -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<ArtistAlbumView> createState() => _ArtistAlbumViewState();
}
class _ArtistAlbumViewState extends State<ArtistAlbumView> {
final PagingController<int, Album> _pagingController =
PagingController<int, Album>(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<SpotifyDI>();
Page<Album> 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<Album>(
itemBuilder: (context, item, index) {
return AlbumCard(item);
},
),
),
),
],
),
);
}
}

View File

@ -3,6 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:spotify/spotify.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/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/helpers/readable-number.dart'; import 'package:spotube/helpers/readable-number.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
@ -20,102 +23,175 @@ class _ArtistProfileState extends State<ArtistProfile> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi; SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: FutureBuilder<Artist>( body: FutureBuilder<Artist>(
future: spotify.artists.get(widget.artistId), future: spotify.artists.get(widget.artistId),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());
} }
return Column( return SingleChildScrollView(
children: [ padding: const EdgeInsets.all(20),
const PageWindowTitleBar( child: Column(
leading: BackButton(), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
Padding( Row(
padding: const EdgeInsets.all(20),
child: Row(
children: [ children: [
const SizedBox(width: 50), const SizedBox(width: 50),
CircleAvatar( CircleAvatar(
maxRadius: 250, radius: MediaQuery.of(context).size.width * 0.18,
minRadius: 100,
backgroundImage: CachedNetworkImageProvider( backgroundImage: CachedNetworkImageProvider(
snapshot.data!.images!.first.url!, snapshot.data!.images!.first.url!,
), ),
), ),
Padding( Flexible(
padding: const EdgeInsets.all(20), child: Padding(
child: Column( padding: const EdgeInsets.all(20),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
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(snapshot.data!.type!.toUpperCase(), borderRadius: BorderRadius.circular(50)),
style: Theme.of(context) child: Text(snapshot.data!.type!.toUpperCase(),
.textTheme style: Theme.of(context)
.headline6 .textTheme
?.copyWith(color: Colors.white)), .headline6
), ?.copyWith(color: Colors.white)),
Text( ),
snapshot.data!.name!, Text(
style: Theme.of(context).textTheme.headline2, snapshot.data!.name!,
), style: Theme.of(context).textTheme.headline2,
Text( ),
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", Text(
style: Theme.of(context).textTheme.headline5, "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
), style: Theme.of(context).textTheme.headline5,
const SizedBox(height: 20), ),
Row( const SizedBox(height: 20),
children: [ Row(
// TODO: Implement check if user follows this artist children: [
// LIMITATION: spotify-dart lib // TODO: Implement check if user follows this artist
FutureBuilder( // LIMITATION: spotify-dart lib
future: Future.value(true), FutureBuilder(
builder: (context, snapshot) { future: Future.value(true),
return OutlinedButton( builder: (context, snapshot) {
onPressed: () async { return OutlinedButton(
// TODO: make `follow/unfollow` artists button work onPressed: () async {
// LIMITATION: spotify-dart lib // TODO: make `follow/unfollow` artists button work
}, // LIMITATION: spotify-dart lib
child: Text(snapshot.data == true },
? "Following" child: Text(snapshot.data == true
: "Follow"), ? "Following"
); : "Follow"),
}), );
IconButton( }),
icon: const Icon(Icons.share_rounded), IconButton(
onPressed: () { icon: const Icon(Icons.share_rounded),
Clipboard.setData( onPressed: () {
ClipboardData( Clipboard.setData(
text: snapshot ClipboardData(
.data?.externalUrls?.spotify), text: snapshot
).then((val) { .data?.externalUrls?.spotify),
ScaffoldMessenger.of(context).showSnackBar( ).then((val) {
const SnackBar( ScaffoldMessenger.of(context)
width: 300, .showSnackBar(
behavior: SnackBarBehavior.floating, const SnackBar(
content: Text( width: 300,
"Artist URL copied to clipboard", behavior: SnackBarBehavior.floating,
textAlign: TextAlign.center, 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<List<Album>>(
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<Iterable<Artist>>(
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() ??
[],
),
);
},
)
],
),
); );
}, },
), ),

View File

@ -13,8 +13,8 @@ class UserArtists extends StatefulWidget {
} }
class _UserArtistsState extends State<UserArtists> { class _UserArtistsState extends State<UserArtists> {
final PagingController<int, Artist> _pagingController = final PagingController<String, Artist> _pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: "");
@override @override
void initState() { void initState() {
@ -23,22 +23,16 @@ class _UserArtistsState extends State<UserArtists> {
_pagingController.addPageRequestListener((pageKey) async { _pagingController.addPageRequestListener((pageKey) async {
try { try {
SpotifyDI data = context.read<SpotifyDI>(); SpotifyDI data = context.read<SpotifyDI>();
var offset =
_pagingController.value.itemList?.elementAt(pageKey).id ?? "";
CursorPage<Artist> artists = await data.spotifyApi.me CursorPage<Artist> artists = await data.spotifyApi.me
.following(FollowingType.artist) .following(FollowingType.artist)
.getPage(15, offset); .getPage(15, pageKey);
var items = artists.items!.toList(); var items = artists.items!.toList();
if (artists.items != null && items.length < 15) { if (artists.items != null && items.length < 15) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);
} else if (artists.items != null) { } else if (artists.items != null) {
var yetToBe = [ _pagingController.appendPage(items, items.last.id);
...(_pagingController.value.itemList ?? []),
...items
];
_pagingController.appendPage(items, yetToBe.length - 1);
} }
} catch (e, stack) { } catch (e, stack) {
_pagingController.error = e; _pagingController.error = e;
@ -49,6 +43,12 @@ class _UserArtistsState extends State<UserArtists> {
}); });
} }
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SpotifyDI data = context.watch<SpotifyDI>(); SpotifyDI data = context.watch<SpotifyDI>();

View File

@ -34,75 +34,69 @@ class _LoginState extends State<Login> {
return Consumer<Auth>( return Consumer<Auth>(
builder: (context, authState, child) { builder: (context, authState, child) {
return Scaffold( return Scaffold(
body: Column( appBar: const PageWindowTitleBar(),
children: [ body: Center(
const PageWindowTitleBar(), child: Column(
Expanded( mainAxisAlignment: MainAxisAlignment.center,
child: 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Image.asset( TextField(
"assets/spotube-logo.png", decoration: const InputDecoration(
width: 400, hintText: "Spotify Client ID",
height: 400, 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( const SizedBox(
height: 10, height: 10,
), ),
Container( TextField(
constraints: const BoxConstraints( decoration: const InputDecoration(
maxWidth: 400, hintText: "Spotify Client Secret",
), label: Text("Client Secret"),
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"),
)
],
), ),
onChanged: (value) {
setState(() {
clientSecret = value;
});
},
), ),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () {
handleLogin(authState);
},
child: const Text("Submit"),
)
], ],
), ),
), ),
), ],
], ),
), ),
); );
}, },

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Settings.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/helpers/getLyrics.dart';
@ -29,7 +30,7 @@ class _LyricsState extends State<Lyrics> {
playback.currentTrack!.id != _lyrics["id"]) { playback.currentTrack!.id != _lyrics["id"]) {
getLyrics( getLyrics(
playback.currentTrack!.name!, playback.currentTrack!.name!,
artistsToString(playback.currentTrack!.artists ?? []), artistsToString<Artist>(playback.currentTrack!.artists ?? []),
apiKey: userPreferences.geniusAccessToken!, apiKey: userPreferences.geniusAccessToken!,
optimizeQuery: true, optimizeQuery: true,
).then((lyrics) { ).then((lyrics) {
@ -90,7 +91,7 @@ class _LyricsState extends State<Lyrics> {
), ),
Center( Center(
child: Text( child: Text(
artistsToString(playback.currentTrack?.artists ?? []), artistsToString<Artist>(playback.currentTrack?.artists ?? []),
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
), ),
), ),

View File

@ -240,8 +240,8 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
playback.currentTrack?.name ?? "Not playing", playback.currentTrack?.name ?? "Not playing",
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
Text( Text(artistsToString<Artist>(
artistsToString(playback.currentTrack?.artists ?? [])) playback.currentTrack?.artists ?? []))
], ],
), ),
), ),

View File

@ -1,8 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistView.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/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
@ -16,7 +16,13 @@ class PlaylistCard extends StatefulWidget {
class _PlaylistCardState extends State<PlaylistCard> { class _PlaylistCardState extends State<PlaylistCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( Playback playback = context.watch<Playback>();
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: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) { builder: (context) {
@ -24,108 +30,29 @@ class _PlaylistCardState extends State<PlaylistCard> {
}, },
)); ));
}, },
child: ConstrainedBox( onPlaybuttonPressed: () async {
constraints: const BoxConstraints(maxWidth: 200), if (isPlaylistPlaying) return;
child: Ink( SpotifyDI data = context.read<SpotifyDI>();
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<Playback>();
SpotifyDI data = context.watch<SpotifyDI>();
bool isPlaylistPlaying = playback.currentPlaylist !=
null &&
playback.currentPlaylist!.id == widget.playlist.id;
return ElevatedButton(
onPressed: () async {
if (isPlaylistPlaying) return;
List<Track> tracks = List<Track> tracks = (widget.playlist.id != "user-liked-tracks"
(widget.playlist.id != "user-liked-tracks" ? await data.spotifyApi.playlists
? await data.spotifyApi.playlists .getTracksByPlaylistId(widget.playlist.id!)
.getTracksByPlaylistId( .all()
widget.playlist.id!) : await data.spotifyApi.tracks.me.saved
.all() .all()
: await data.spotifyApi.tracks.me.saved .then((tracks) => tracks.map((e) => e.track!)))
.all() .toList();
.then((tracks) =>
tracks.map((e) => e.track!)))
.toList();
if (tracks.isEmpty) return; if (tracks.isEmpty) return;
playback.setCurrentPlaylist = CurrentPlaylist( playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks, tracks: tracks,
id: widget.playlist.id!, id: widget.playlist.id!,
name: widget.playlist.name!, name: widget.playlist.name!,
thumbnail: widget.playlist.images!.first.url!, thumbnail: widget.playlist.images!.first.url!,
); );
playback.setCurrentTrack = tracks.first; 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),
),
],
),
)
],
),
),
),
); );
} }
} }

View File

@ -23,11 +23,11 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: Column( body: Column(
children: [ children: [
const PageWindowTitleBar(
leading: BackButton(),
),
Text( Text(
widget.genreName, widget.genreName,
style: Theme.of(context).textTheme.headline4, style: Theme.of(context).textTheme.headline4,
@ -51,15 +51,17 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const CircularProgressIndicator.adaptive(); return const CircularProgressIndicator.adaptive();
} }
return Wrap( return Center(
children: snapshot.data! child: Wrap(
.map( children: snapshot.data!
(playlist) => Padding( .map(
padding: const EdgeInsets.all(8.0), (playlist) => Padding(
child: PlaylistCard(playlist), padding: const EdgeInsets.all(8.0),
), child: PlaylistCard(playlist),
) ),
.toList(), )
.toList(),
),
); );
}), }),
), ),

View File

@ -40,112 +40,107 @@ class _SettingsState extends State<Settings> {
UserPreferences preferences = context.watch<UserPreferences>(); UserPreferences preferences = context.watch<UserPreferences>();
return Scaffold( return Scaffold(
body: Column( appBar: PageWindowTitleBar(
children: [ leading: const BackButton(),
PageWindowTitleBar( center: Text(
leading: const BackButton(), "Settings",
center: Text( style: Theme.of(context).textTheme.headline5,
"Settings", ),
style: Theme.of(context).textTheme.headline5, ),
), body: Padding(
), padding: const EdgeInsets.all(16.0),
const SizedBox(height: 10), child: Column(
Padding( children: [
padding: const EdgeInsets.all(16.0), Row(
child: Column(
children: [ children: [
Row( Expanded(
children: [ flex: 2,
Expanded( child: Text(
flex: 2, "Genius Access Token",
child: Text( style: Theme.of(context).textTheme.subtitle1,
"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"),
),
)
],
), ),
const SizedBox(height: 10), Expanded(
Row( flex: 1,
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: TextField(
children: [ controller: _textEditingController,
const Text("Theme"), decoration: InputDecoration(
DropdownButton<ThemeMode>( hintText: preferences.geniusAccessToken,
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), Padding(
Builder(builder: (context) { padding: const EdgeInsets.all(8.0),
var auth = context.read<Auth>(); child: ElevatedButton(
return ElevatedButton( onPressed: _geniusAccessToken != null
child: const Text("Logout"), ? () async {
onPressed: () async { SharedPreferences localStorage =
SharedPreferences localStorage = await SharedPreferences.getInstance();
await SharedPreferences.getInstance(); preferences
await localStorage.clear(); .setGeniusAccessToken(_geniusAccessToken);
auth.logout(); localStorage.setString(
Navigator.of(context).pop(); 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<ThemeMode>(
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<Auth>();
return ElevatedButton(
child: const Text("Logout"),
onPressed: () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
await localStorage.clear();
auth.logout();
Navigator.of(context).pop();
},
);
})
],
),
), ),
); );
} }

View File

@ -70,7 +70,7 @@ class _DownloadTrackButtonState extends State<DownloadTrackButton> {
String downloadFolder = path.join( String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path, "Spotube"); (await path_provider.getDownloadsDirectory())!.path, "Spotube");
String fileName = String fileName =
"${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3"; "${widget.track?.name} - ${artistsToString<Artist>(widget.track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName)); File outputFile = File(path.join(downloadFolder, fileName));
if (!outputFile.existsSync()) { if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true); outputFile.createSync(recursive: true);

View File

@ -43,11 +43,15 @@ class TitleBarActionButtons extends StatelessWidget {
} }
} }
class PageWindowTitleBar extends StatelessWidget { class PageWindowTitleBar extends StatelessWidget
implements PreferredSizeWidget {
final Widget? leading; final Widget? leading;
final Widget? center; final Widget? center;
const PageWindowTitleBar({Key? key, this.leading, this.center}) const PageWindowTitleBar({Key? key, this.leading, this.center})
: super(key: key); : super(key: key);
@override
Size get preferredSize => Size.fromHeight(appWindow.titleBarHeight);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WindowTitleBarBox( return WindowTitleBarBox(

View File

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

View File

@ -1,5 +1,5 @@
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
String artistsToString(List<Artist> artists) { String artistsToString<T extends ArtistSimple>(List<T> artists) {
return artists.map((e) => e.name?.replaceAll(",", " ")).join(", "); return artists.map((e) => e.name?.replaceAll(",", " ")).join(", ");
} }