mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
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:
parent
46b652788f
commit
ef121c3613
10
.github/workflows/flutter-build.yml
vendored
10
.github/workflows/flutter-build.yml
vendored
@ -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
|
||||
|
27
lib/components/Album/AlbumCard.dart
Normal file
27
lib/components/Album/AlbumCard.dart
Normal 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: () => {},
|
||||
);
|
||||
}
|
||||
}
|
92
lib/components/Artist/ArtistAlbumView.dart
Normal file
92
lib/components/Artist/ArtistAlbumView.dart
Normal 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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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() ??
|
||||
[],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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>();
|
||||
|
@ -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"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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 ?? []))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
@ -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();
|
||||
},
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
110
lib/components/Shared/PlaybuttonCard.dart
Normal file
110
lib/components/Shared/PlaybuttonCard.dart
Normal 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,
|
||||
),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(", ");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user