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:
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

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: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<ArtistProfile> {
Widget build(BuildContext context) {
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: FutureBuilder<Artist>(
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<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> {
final PagingController<int, Artist> _pagingController =
PagingController(firstPageKey: 0);
final PagingController<String, Artist> _pagingController =
PagingController(firstPageKey: "");
@override
void initState() {
@ -23,22 +23,16 @@ class _UserArtistsState extends State<UserArtists> {
_pagingController.addPageRequestListener((pageKey) async {
try {
SpotifyDI data = context.read<SpotifyDI>();
var offset =
_pagingController.value.itemList?.elementAt(pageKey).id ?? "";
CursorPage<Artist> 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<UserArtists> {
});
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
SpotifyDI data = context.watch<SpotifyDI>();

View File

@ -34,75 +34,69 @@ class _LoginState extends State<Login> {
return Consumer<Auth>(
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"),
)
],
),
),
),
],
],
),
),
);
},

View File

@ -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<Lyrics> {
playback.currentTrack!.id != _lyrics["id"]) {
getLyrics(
playback.currentTrack!.name!,
artistsToString(playback.currentTrack!.artists ?? []),
artistsToString<Artist>(playback.currentTrack!.artists ?? []),
apiKey: userPreferences.geniusAccessToken!,
optimizeQuery: true,
).then((lyrics) {
@ -90,7 +91,7 @@ class _LyricsState extends State<Lyrics> {
),
Center(
child: Text(
artistsToString(playback.currentTrack?.artists ?? []),
artistsToString<Artist>(playback.currentTrack?.artists ?? []),
style: Theme.of(context).textTheme.headline5,
),
),

View File

@ -240,8 +240,8 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
playback.currentTrack?.name ?? "Not playing",
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
artistsToString(playback.currentTrack?.artists ?? []))
Text(artistsToString<Artist>(
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: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<PlaylistCard> {
@override
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: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
@ -24,108 +30,29 @@ class _PlaylistCardState extends State<PlaylistCard> {
},
));
},
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<Playback>();
SpotifyDI data = context.watch<SpotifyDI>();
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<SpotifyDI>();
List<Track> 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<Track> 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;
},
);
}
}

View File

@ -23,11 +23,11 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@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<PlaylistGenreView> {
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(),
),
);
}),
),

View File

@ -40,112 +40,107 @@ class _SettingsState extends State<Settings> {
UserPreferences preferences = context.watch<UserPreferences>();
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<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);
}
},
)
],
Expanded(
flex: 1,
child: TextField(
controller: _textEditingController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
),
),
),
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();
},
);
})
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<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(
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
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));
if (!outputFile.existsSync()) {
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? 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(

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';
String artistsToString(List<Artist> artists) {
String artistsToString<T extends ArtistSimple>(List<T> artists) {
return artists.map((e) => e.name?.replaceAll(",", " ")).join(", ");
}