Infinite Scorll added

CategoryCard no-title bug fix
shuffle support added
This commit is contained in:
Kingkor Roy Tirtho 2022-01-03 13:33:58 +06:00
parent 799e13c376
commit 0ef44709fa
10 changed files with 382 additions and 326 deletions

View File

@ -7,7 +7,7 @@ import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget {
final Category category;
CategoryCard(this.category);
const CategoryCard(this.category, {Key? key}) : super(key: key);
@override
_CategoryCardState createState() => _CategoryCardState();
@ -16,8 +16,7 @@ class CategoryCard extends StatefulWidget {
class _CategoryCardState extends State<CategoryCard> {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
@ -33,12 +32,15 @@ class _CategoryCardState extends State<CategoryCard> {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return PlaylistGenreView(widget.category.id!);
return PlaylistGenreView(
widget.category.id!,
widget.category.name!,
);
},
),
);
},
child: Text("See all"),
child: const Text("See all"),
)
],
),
@ -51,10 +53,10 @@ class _CategoryCardState extends State<CategoryCard> {
.getPage(4, 0),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text("Error occurred"));
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return Center(child: Text("Loading.."));
return const Center(child: Text("Loading.."));
}
return Wrap(
spacing: 20,
@ -65,7 +67,6 @@ class _CategoryCardState extends State<CategoryCard> {
}),
)
],
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart' hide Page;
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart';
@ -10,11 +11,16 @@ import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final PagingController<int, Category> _pagingController =
PagingController(firstPageKey: 0);
@override
void initState() {
super.initState();
@ -39,12 +45,35 @@ class _HomeState extends State<Home> {
);
}
}
_pagingController.addPageRequestListener((pageKey) async {
try {
SpotifyDI data = context.read<SpotifyDI>();
Page<Category> categories = await data.spotifyApi.categories
.list(country: "US")
.getPage(15, pageKey);
if (categories.isLast && categories.items != null) {
_pagingController.appendLastPage(categories.items!.toList());
} else if (categories.items != null) {
_pagingController.appendPage(
categories.items!.toList(), categories.nextOffset);
}
} catch (e) {
_pagingController.error = e;
}
});
} catch (e) {
print("[login state error]: $e");
}
});
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Auth authProvider = Provider.of<Auth>(context);
@ -53,8 +82,7 @@ class _HomeState extends State<Home> {
}
return Scaffold(
body: Container(
child: Column(
body: Column(
children: [
// Side Tab Bar
Expanded(
@ -74,9 +102,8 @@ class _HomeState extends State<Home> {
children: [
ListTile(
title: Text("Spotube",
style: Theme.of(context)
.textTheme
.headline4),
style:
Theme.of(context).textTheme.headline4),
leading:
const Icon(Icons.miscellaneous_services),
),
@ -115,31 +142,17 @@ class _HomeState extends State<Home> {
),
// contents of the spotify
Consumer<SpotifyDI>(builder: (_, data, __) {
return FutureBuilder<Page<Category>>(
future: data.spotifyApi.categories
.list(country: "US")
.getPage(10, 0),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occured"));
}
if (!snapshot.hasData) {
return const Center(child: Text("Loading"));
}
List<Category> categories =
snapshot.data!.items!.toList();
return Expanded(
child: Scrollbar(
isAlwaysShown: true,
child: ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
return CategoryCard(categories[index]);
child: PagedListView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Category>(
itemBuilder: (context, item, index) {
return CategoryCard(item);
},
),
)),
),
);
});
}),
],
),
@ -148,7 +161,6 @@ class _HomeState extends State<Home> {
const player.Player()
],
),
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:spotube/components/PlayerControls.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart';
@ -18,8 +20,7 @@ class _PlayerState extends State<Player> {
late MPVPlayer player;
bool _isPlaying = false;
String? _mediaTitle;
String? _mediaArtists;
bool _shuffled = false;
double _duration = 0;
String? _currentPlaylistId;
@ -45,8 +46,10 @@ class _PlayerState extends State<Player> {
_volume = volume / 100;
});
} catch (e) {
if (kDebugMode) {
print("[PLAYER]: $e");
}
}
})();
player.on(MPVEvents.paused, null, (ev, context) {
@ -65,20 +68,27 @@ class _PlayerState extends State<Player> {
player.on(MPVEvents.status, null, (ev, _) async {
Map data = ev.eventData as Map;
Playback playback = context.read<Playback>();
print("[DATA]: $data");
if (data["property"] == "media-title" && data["value"] != null) {
var containsYtdl = (data["value"] as String).contains("ytsearch:");
if (containsYtdl) {
var props = (data["value"] as String).split("-");
var mediaTitle = props.last.trim();
var mediaArtists = props.first.split("ytsearch:").last.trim();
setState(() {
_isPlaying = true;
_mediaTitle = props.last.replaceAll(
RegExp(
"(official|video|lyric|[(){}\\[\\]\\|])",
caseSensitive: false,
),
"",
);
_mediaArtists = props.first;
});
var matchedTracks = playback.currentPlaylist?.tracks.where(
(track) {
return track.name == mediaTitle &&
artistsToString(track.artists ?? []) == mediaArtists;
},
) ??
[];
if (matchedTracks.isNotEmpty) {
playback.setCurrentTrack = matchedTracks.first;
}
}
}
if (data["property"] == "duration" && data["value"] != null) {
setState(() {
@ -90,30 +100,6 @@ class _PlayerState extends State<Player> {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
Playback playback = context.read<Playback>();
String? prevTrackName = playback.currentTrack?.name;
String prevTrackArtists =
artistsToString(playback.currentTrack?.artists ?? []);
if (playback.currentPlaylist != null &&
playback.currentPlaylist!.tracks.isNotEmpty &&
prevTrackName != _mediaTitle &&
prevTrackArtists != _mediaArtists) {
var tracks = playback.currentPlaylist?.tracks.where((track) {
return _mediaTitle == track.name! &&
artistsToString(track.artists ?? []) == _mediaTitle;
}) ??
[];
if (tracks.isNotEmpty) {
playback.setCurrentTrack = tracks.first;
}
}
}
@override
void dispose() {
player.removeAllByEvent(MPVEvents.paused);
@ -134,7 +120,6 @@ class _PlayerState extends State<Player> {
File file = File(playlistPath);
var newPlaylist = playlistToStr(playlist);
print("😃PLAYING PLAYLIST😃");
if (!await file.exists()) {
await file.create();
}
@ -144,6 +129,7 @@ class _PlayerState extends State<Player> {
await player.loadPlaylist(playlistPath);
setState(() {
_currentPlaylistId = playlist.id;
_shuffled = false;
});
}
}
@ -162,21 +148,30 @@ class _PlayerState extends State<Player> {
playPlaylist(playback.currentPlaylist!);
}
String? albumArt = playback.currentTrack?.album?.images?.last.url;
return Material(
type: MaterialType.transparency,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (albumArt != null)
CachedNetworkImage(
imageUrl: albumArt,
maxHeightDiskCache: 50,
maxWidthDiskCache: 50,
),
// title of the currently playing track
Flexible(
flex: 1,
child: Column(
children: [
Text(
_mediaTitle ?? "Not playing",
playback.currentTrack?.name ?? "Not playing",
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(_mediaArtists ?? "")
Text(
artistsToString(playback.currentTrack?.artists ?? []))
],
),
),
@ -187,13 +182,28 @@ class _PlayerState extends State<Player> {
player: player,
isPlaying: _isPlaying,
duration: _duration,
shuffled: _shuffled,
onShuffle: () {
if (!_shuffled) {
player.shuffle().then(
(value) => setState(() {
_shuffled = true;
}),
);
} else {
player.unshuffle().then(
(value) => setState(() {
_shuffled = false;
}),
);
}
},
onStop: () {
setState(() {
_isPlaying = false;
_currentPlaylistId = null;
_mediaArtists = null;
_mediaTitle = null;
_duration = 0;
_shuffled = false;
});
playback.reset();
},

View File

@ -5,11 +5,15 @@ class PlayerControls extends StatefulWidget {
final MPVPlayer player;
final bool isPlaying;
final double duration;
final bool shuffled;
final Function? onStop;
final Function? onShuffle;
const PlayerControls({
required this.player,
required this.isPlaying,
required this.duration,
required this.shuffled,
this.onShuffle,
this.onStop,
Key? key,
}) : super(key: key);
@ -86,8 +90,10 @@ class _PlayerControlsState extends State<PlayerControls> {
children: [
IconButton(
icon: const Icon(Icons.shuffle_rounded),
onPressed: () async {
await widget.player.shuffle();
color:
widget.shuffled ? Theme.of(context).primaryColor : null,
onPressed: () {
widget.onShuffle?.call();
}),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),

View File

@ -5,8 +5,10 @@ import 'package:spotube/components/PlaylistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends StatefulWidget {
String genre_id;
PlaylistGenreView(this.genre_id);
final String genreId;
final String genreName;
const PlaylistGenreView(this.genreId, this.genreName, {Key? key})
: super(key: key);
@override
_PlaylistGenreViewState createState() => _PlaylistGenreViewState();
}
@ -15,17 +17,16 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
body: Column(
children: [
Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
BackButton(),
const BackButton(),
// genre name
Expanded(
child: Text(
"Genre Name",
widget.genreName,
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
@ -37,14 +38,14 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
child: SingleChildScrollView(
child: FutureBuilder<Iterable<PlaylistSimple>>(
future: data.spotifyApi.playlists
.getByCategoryId(widget.genre_id)
.getByCategoryId(widget.genreId)
.all(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text("Error occurred"));
return const Center(child: Text("Error occurred"));
}
if (!snapshot.hasData) {
return Center(child: Text("Loading.."));
return const Center(child: Text("Loading.."));
}
return Wrap(
children: snapshot.data!
@ -62,7 +63,6 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
)
],
),
),
);
}
}

View File

@ -6,8 +6,8 @@ import 'package:spotube/components/TrackButton.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistView extends StatefulWidget {
PlaylistSimple playlist;
PlaylistView(this.playlist);
final PlaylistSimple playlist;
const PlaylistView(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistViewState createState() => _PlaylistViewState();
}
@ -18,19 +18,12 @@ class _PlaylistViewState extends State<PlaylistView> {
Playback playback = context.read<Playback>();
return Consumer<SpotifyDI>(builder: (_, data, __) {
return Scaffold(
body: Container(
child: FutureBuilder<Iterable<Track>>(
body: FutureBuilder<Iterable<Track>>(
future: data.spotifyApi.playlists
.getTracksByPlaylistId(widget.playlist.id)
.all(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: const Text("Error occurred"));
}
if (!snapshot.hasData) {
return const Center(child: const Text("Loading.."));
}
List<Track> tracks = snapshot.data!.toList();
List<Track> tracks = snapshot.data?.toList() ?? [];
return Column(
children: [
Row(
@ -43,24 +36,42 @@ class _PlaylistViewState extends State<PlaylistView> {
onPressed: () {},
),
// play playlist
IconButton(
icon: const Icon(Icons.play_arrow_rounded),
onPressed: () {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: widget.playlist.id!,
name: widget.playlist.name!,
thumbnail: widget.playlist.images![0].url!,
);
},
Consumer<Playback>(builder: (context, playback, widget) {
var isPlaylistPlaying = playback.currentPlaylist?.id ==
this.widget.playlist.id;
return IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () {
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist =
CurrentPlaylist(
tracks: tracks,
id: this.widget.playlist.id!,
name: this.widget.playlist.name!,
thumbnail:
this.widget.playlist.images![0].url!,
);
}
}
: null,
);
}),
],
),
Center(
child: Text(widget.playlist.name!,
style: Theme.of(context).textTheme.headline4),
),
Expanded(
snapshot.hasError
? const Center(child: Text("Error occurred"))
: !snapshot.hasData
? const Center(child: Text("Loading.."))
: Expanded(
child: Scrollbar(
isAlwaysShown: true,
child: ListView.builder(
@ -82,14 +93,16 @@ class _PlaylistViewState extends State<PlaylistView> {
Track track = tracks[index - 1];
return TrackButton(
index: (index - 1).toString(),
thumbnail_url: track.album?.images?.last.url ??
thumbnail_url: track
.album?.images?.last.url ??
"https://i.scdn.co/image/ab67616d00001e02b993cba8ff7d0a8e9ee18d46",
trackName: track.name!,
artists:
track.artists!.map((e) => e.name!).toList(),
artists: track.artists!
.map((e) => e.name!)
.toList(),
album: track.album!.name!,
playback_time:
track.duration!.inMinutes.toString(),
playback_time: track.duration!.inMinutes
.toString(),
onTap: () {},
);
}),
@ -98,7 +111,6 @@ class _PlaylistViewState extends State<PlaylistView> {
],
);
}),
),
);
});
}

View File

@ -30,7 +30,7 @@ class _TrackButtonState extends State<TrackButton> {
child: InkWell(
onTap: widget.onTap,
child: Ink(
padding: EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -38,16 +38,16 @@ class _TrackButtonState extends State<TrackButton> {
children: [
Text(
widget.index,
style: TextStyle(fontSize: 20),
style: const TextStyle(fontSize: 20),
),
SizedBox(width: 15),
const SizedBox(width: 15),
if (widget.thumbnail_url != null)
CachedNetworkImage(
imageUrl: widget.thumbnail_url!,
maxHeightDiskCache: 50,
maxWidthDiskCache: 50,
),
SizedBox(width: 15),
const SizedBox(width: 15),
Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -55,7 +55,7 @@ class _TrackButtonState extends State<TrackButton> {
Text(
widget.trackName,
textAlign: TextAlign.justify,
style: TextStyle(
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 17),
),
Text(widget.artists.join(", "))
@ -64,9 +64,9 @@ class _TrackButtonState extends State<TrackButton> {
),
],
),
SizedBox(width: 15),
const SizedBox(width: 15),
Text(widget.album),
SizedBox(width: 15),
const SizedBox(width: 15),
Text(widget.playback_time)
],
),

View File

@ -29,8 +29,8 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
primaryColor: Colors.greenAccent[400],
primarySwatch: Colors.green,
buttonTheme: ButtonThemeData(
buttonColor: Colors.greenAccent[400],
buttonTheme: const ButtonThemeData(
buttonColor: Colors.green,
),
),
home: Home(),

View File

@ -177,6 +177,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
infinite_scroll_pagination:
dependency: "direct main"
description:
name: infinite_scroll_pagination
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
js:
dependency: transitive
description:
@ -406,6 +413,13 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
sliver_tools:
dependency: transitive
description:
name: sliver_tools
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
source_span:
dependency: transitive
description:

View File

@ -44,6 +44,7 @@ dependencies:
youtube_explode_dart: ^1.10.8
mpv_dart:
path: ../mpv_dart
infinite_scroll_pagination: ^3.1.0
dev_dependencies:
flutter_test: