mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Working Sidebar added
Unauthorized error fix Settings Page (initial) Redesinged PlaylistView with Table
This commit is contained in:
parent
809731f441
commit
76d0538f96
BIN
assets/spotube-logo.png
Normal file
BIN
assets/spotube-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
@ -1,15 +1,26 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart' hide Page;
|
import 'package:flutter/material.dart' hide Page;
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
import 'package:spotube/components/CategoryCard.dart';
|
import 'package:spotube/components/CategoryCard.dart';
|
||||||
import 'package:spotube/components/Login.dart';
|
import 'package:spotube/components/Login.dart';
|
||||||
import 'package:spotube/components/Player.dart' as player;
|
import 'package:spotube/components/Player.dart' as player;
|
||||||
|
import 'package:spotube/components/Settings.dart';
|
||||||
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/models/sideBarTiles.dart';
|
import 'package:spotube/models/sideBarTiles.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
|
|
||||||
|
List<String> spotifyScopes = [
|
||||||
|
"user-library-read",
|
||||||
|
"user-library-modify",
|
||||||
|
"user-read-private",
|
||||||
|
"user-read-email",
|
||||||
|
"playlist-read-collaborative"
|
||||||
|
];
|
||||||
|
|
||||||
class Home extends StatefulWidget {
|
class Home extends StatefulWidget {
|
||||||
const Home({Key? key}) : super(key: key);
|
const Home({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@ -21,6 +32,8 @@ class _HomeState extends State<Home> {
|
|||||||
final PagingController<int, Category> _pagingController =
|
final PagingController<int, Category> _pagingController =
|
||||||
PagingController(firstPageKey: 0);
|
PagingController(firstPageKey: 0);
|
||||||
|
|
||||||
|
int _selectedIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -28,24 +41,36 @@ class _HomeState extends State<Home> {
|
|||||||
try {
|
try {
|
||||||
Auth authProvider = context.read<Auth>();
|
Auth authProvider = context.read<Auth>();
|
||||||
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
||||||
String? clientId = localStorage.getString('client_id');
|
var clientId = localStorage.getString(LocalStorageKeys.clientId);
|
||||||
String? clientSecret = localStorage.getString('client_secret');
|
var clientSecret =
|
||||||
|
localStorage.getString(LocalStorageKeys.clientSecret);
|
||||||
|
var accessToken = localStorage.getString(LocalStorageKeys.accessToken);
|
||||||
|
var refreshToken =
|
||||||
|
localStorage.getString(LocalStorageKeys.refreshToken);
|
||||||
|
var expirationStr = localStorage.getString(LocalStorageKeys.expiration);
|
||||||
|
var expiration =
|
||||||
|
expirationStr != null ? DateTime.parse(expirationStr) : null;
|
||||||
|
|
||||||
if (clientId != null && clientSecret != null) {
|
if (clientId != null && clientSecret != null) {
|
||||||
SpotifyApi spotifyApi = SpotifyApi(
|
SpotifyApi spotifyApi = SpotifyApi(
|
||||||
SpotifyApiCredentials(clientId, clientSecret, scopes: [
|
SpotifyApiCredentials(
|
||||||
"user-library-read",
|
clientId,
|
||||||
"user-library-modify",
|
clientSecret,
|
||||||
"user-read-private",
|
accessToken: accessToken,
|
||||||
"user-read-email",
|
refreshToken: refreshToken,
|
||||||
"playlist-read-collaborative"
|
expiration: expiration,
|
||||||
]),
|
scopes: spotifyScopes,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
SpotifyApiCredentials credentials = await spotifyApi.getCredentials();
|
SpotifyApiCredentials credentials = await spotifyApi.getCredentials();
|
||||||
if (credentials.accessToken?.isNotEmpty ?? false) {
|
if (credentials.accessToken?.isNotEmpty ?? false) {
|
||||||
authProvider.setAuthState(
|
authProvider.setAuthState(
|
||||||
clientId: credentials.clientId,
|
clientId: clientId,
|
||||||
clientSecret: credentials.clientSecret,
|
clientSecret: clientSecret,
|
||||||
|
accessToken:
|
||||||
|
credentials.accessToken, // accessToken can be new/refreshed
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
expiration: credentials.expiration,
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -89,87 +114,98 @@ class _HomeState extends State<Home> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Side Tab Bar
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
NavigationRail(
|
||||||
color: Colors.blueGrey[50],
|
backgroundColor: Colors.blueGrey[50],
|
||||||
constraints: const BoxConstraints(maxWidth: 230),
|
destinations: sidebarTileList
|
||||||
child: Material(
|
.map((e) => NavigationRailDestination(
|
||||||
type: MaterialType.transparency,
|
icon: Icon(e.icon),
|
||||||
child: Column(
|
label: Text(
|
||||||
children: [
|
e.title,
|
||||||
Flexible(
|
style: const TextStyle(
|
||||||
flex: 1,
|
fontWeight: FontWeight.bold,
|
||||||
// TabButtons
|
fontSize: 16,
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
title: Text("Spotube",
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.headline4),
|
|
||||||
leading:
|
|
||||||
const Icon(Icons.miscellaneous_services),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
),
|
||||||
...sidebarTileList
|
))
|
||||||
.map(
|
.toList(),
|
||||||
(sidebarTile) => ListTile(
|
selectedIndex: _selectedIndex,
|
||||||
title: Text(sidebarTile.title),
|
onDestinationSelected: (value) => setState(() {
|
||||||
leading: Icon(sidebarTile.icon),
|
_selectedIndex = value;
|
||||||
onTap: () {},
|
}),
|
||||||
),
|
extended: true,
|
||||||
)
|
leading: Padding(
|
||||||
.toList(),
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: Row(children: [
|
||||||
|
Image.asset(
|
||||||
|
"assets/spotube-logo.png",
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text("Spotube",
|
||||||
|
style: Theme.of(context).textTheme.headline4),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
trailing:
|
||||||
|
Consumer<SpotifyDI>(builder: (context, data, widget) {
|
||||||
|
return FutureBuilder<User>(
|
||||||
|
future: data.spotifyApi.me.get(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
var avatarImg = snapshot.data?.images?.last.url;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
if (avatarImg != null)
|
||||||
|
CircleAvatar(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: avatarImg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
snapshot.data?.displayName ?? "User's name",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings_outlined),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return const Settings();
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
// user name & settings
|
},
|
||||||
Consumer<SpotifyDI>(builder: (context, data, widget) {
|
);
|
||||||
return FutureBuilder<User>(
|
}),
|
||||||
future: data.spotifyApi.me.get(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
snapshot.data?.displayName ??
|
|
||||||
"User's name",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon:
|
|
||||||
const Icon(Icons.settings_outlined),
|
|
||||||
onPressed: () {}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// contents of the spotify
|
// contents of the spotify
|
||||||
Expanded(
|
if (_selectedIndex == 0)
|
||||||
child: Scrollbar(
|
Expanded(
|
||||||
child: PagedListView(
|
child: Scrollbar(
|
||||||
pagingController: _pagingController,
|
child: PagedListView(
|
||||||
builderDelegate: PagedChildBuilderDelegate<Category>(
|
pagingController: _pagingController,
|
||||||
itemBuilder: (context, item, index) {
|
builderDelegate: PagedChildBuilderDelegate<Category>(
|
||||||
return CategoryCard(item);
|
itemBuilder: (context, item, index) {
|
||||||
},
|
return CategoryCard(item);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// player itself
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -2,8 +2,10 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
|
import 'package:spotube/components/Home.dart';
|
||||||
import 'package:spotube/helpers/server_ipc.dart';
|
import 'package:spotube/helpers/server_ipc.dart';
|
||||||
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.dart';
|
||||||
|
|
||||||
class Login extends StatefulWidget {
|
class Login extends StatefulWidget {
|
||||||
@ -12,38 +14,65 @@ class Login extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LoginState extends State<Login> {
|
class _LoginState extends State<Login> {
|
||||||
String client_id = "";
|
String clientId = "";
|
||||||
String client_secret = "";
|
String clientSecret = "";
|
||||||
bool _fieldError = false;
|
bool _fieldError = false;
|
||||||
|
String? accessToken;
|
||||||
|
String? refreshToken;
|
||||||
|
DateTime? expiration;
|
||||||
|
|
||||||
handleLogin(Auth authState) async {
|
handleLogin(Auth authState) async {
|
||||||
try {
|
try {
|
||||||
if (client_id == "" || client_secret == "") {
|
if (clientId == "" || clientSecret == "") {
|
||||||
return setState(() {
|
return setState(() {
|
||||||
_fieldError = true;
|
_fieldError = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
final credentials = SpotifyApiCredentials(client_id, client_secret);
|
final credentials = SpotifyApiCredentials(clientId, clientSecret);
|
||||||
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
final grant = SpotifyApi.authorizationCodeGrant(credentials);
|
||||||
final redirectUri = "http://localhost:4304/auth/spotify/callback";
|
const redirectUri = "http://localhost:4304/auth/spotify/callback";
|
||||||
final scopes = ["user-library-read", "user-library-modify"];
|
|
||||||
|
|
||||||
final authUri =
|
final authUri = grant.getAuthorizationUrl(Uri.parse(redirectUri),
|
||||||
grant.getAuthorizationUrl(Uri.parse(redirectUri), scopes: scopes);
|
scopes: spotifyScopes);
|
||||||
|
|
||||||
final responseUri = await connectIpc(authUri.toString(), redirectUri);
|
final responseUri = await connectIpc(authUri.toString(), redirectUri);
|
||||||
|
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
||||||
if (responseUri != null) {
|
if (responseUri != null) {
|
||||||
final SpotifyApi spotify =
|
final SpotifyApi spotify =
|
||||||
SpotifyApi.fromAuthCodeGrant(grant, responseUri);
|
SpotifyApi.fromAuthCodeGrant(grant, responseUri);
|
||||||
|
var credentials = await spotify.getCredentials();
|
||||||
|
if (credentials.accessToken != null) {
|
||||||
|
accessToken = credentials.accessToken;
|
||||||
|
await localStorage.setString(
|
||||||
|
LocalStorageKeys.accessToken, credentials.accessToken!);
|
||||||
|
}
|
||||||
|
if (credentials.refreshToken != null) {
|
||||||
|
refreshToken = credentials.refreshToken;
|
||||||
|
await localStorage.setString(
|
||||||
|
LocalStorageKeys.refreshToken, credentials.refreshToken!);
|
||||||
|
}
|
||||||
|
if (credentials.expiration != null) {
|
||||||
|
expiration = credentials.expiration;
|
||||||
|
await localStorage.setString(LocalStorageKeys.expiration,
|
||||||
|
credentials.expiration?.toString() ?? "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
await localStorage.setString(LocalStorageKeys.clientId, clientId);
|
||||||
await localStorage.setString('client_id', client_id);
|
await localStorage.setString(
|
||||||
await localStorage.setString('client_secret', client_secret);
|
LocalStorageKeys.clientSecret,
|
||||||
|
clientSecret,
|
||||||
|
);
|
||||||
authState.setAuthState(
|
authState.setAuthState(
|
||||||
clientId: client_id, clientSecret: client_secret, isLoggedIn: true);
|
clientId: clientId,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
expiration: expiration,
|
||||||
|
isLoggedIn: true,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(e);
|
print("[Login.handleLogin] $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +86,11 @@ class _LoginState extends State<Login> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
"assets/spotube-logo.png",
|
||||||
|
width: 400,
|
||||||
|
height: 400,
|
||||||
|
),
|
||||||
Text("Add your spotify credentials to get started",
|
Text("Add your spotify credentials to get started",
|
||||||
style: Theme.of(context).textTheme.headline4),
|
style: Theme.of(context).textTheme.headline4),
|
||||||
const Text(
|
const Text(
|
||||||
@ -77,7 +111,7 @@ class _LoginState extends State<Login> {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
client_id = value;
|
clientId = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -91,7 +125,7 @@ class _LoginState extends State<Login> {
|
|||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
client_secret = value;
|
clientSecret = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mpv_dart/mpv_dart.dart';
|
import 'package:mpv_dart/mpv_dart.dart';
|
||||||
|
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||||
|
|
||||||
class PlayerControls extends StatefulWidget {
|
class PlayerControls extends StatefulWidget {
|
||||||
final MPVPlayer player;
|
final MPVPlayer player;
|
||||||
@ -43,10 +44,6 @@ class _PlayerControlsState extends State<PlayerControls> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
String zeroPadNumStr(int input) {
|
|
||||||
return input < 10 ? "0$input" : input.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var totalDuration = Duration(seconds: widget.duration.toInt());
|
var totalDuration = Duration(seconds: widget.duration.toInt());
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||||
import 'package:spotube/provider/Playback.dart';
|
import 'package:spotube/provider/Playback.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/TrackButton.dart';
|
|
||||||
import 'package:spotube/provider/SpotifyDI.dart';
|
import 'package:spotube/provider/SpotifyDI.dart';
|
||||||
|
|
||||||
class PlaylistView extends StatefulWidget {
|
class PlaylistView extends StatefulWidget {
|
||||||
@ -13,6 +16,70 @@ class PlaylistView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlaylistViewState extends State<PlaylistView> {
|
class _PlaylistViewState extends State<PlaylistView> {
|
||||||
|
List<TableRow> trackToTableRow(List<Track> tracks) {
|
||||||
|
return tracks.asMap().entries.map((track) {
|
||||||
|
var thumbnailUrl = track.value.album?.images?.last.url;
|
||||||
|
var duration =
|
||||||
|
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||||
|
return (TableRow(
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
child: Text(
|
||||||
|
track.key.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
)),
|
||||||
|
TableCell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (thumbnailUrl != null)
|
||||||
|
CachedNetworkImage(
|
||||||
|
imageUrl: thumbnailUrl,
|
||||||
|
maxHeightDiskCache: 40,
|
||||||
|
maxWidthDiskCache: 40,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
track.value.name ?? "",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 17,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
(track.value.artists ?? [])
|
||||||
|
.map((e) => e.name)
|
||||||
|
.join(", "),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TableCell(
|
||||||
|
child: Text(
|
||||||
|
track.value.album?.name ?? "",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TableCell(
|
||||||
|
child: Text(
|
||||||
|
duration,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<SpotifyDI>(builder: (_, data, __) {
|
return Consumer<SpotifyDI>(builder: (_, data, __) {
|
||||||
@ -23,6 +90,8 @@ class _PlaylistViewState extends State<PlaylistView> {
|
|||||||
.all(),
|
.all(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
List<Track> tracks = snapshot.data?.toList() ?? [];
|
List<Track> tracks = snapshot.data?.toList() ?? [];
|
||||||
|
TextStyle tableHeadStyle =
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -72,39 +141,49 @@ class _PlaylistViewState extends State<PlaylistView> {
|
|||||||
? const CircularProgressIndicator.adaptive()
|
? const CircularProgressIndicator.adaptive()
|
||||||
: Expanded(
|
: Expanded(
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
isAlwaysShown: true,
|
child: ListView(
|
||||||
child: ListView.builder(
|
children: [
|
||||||
itemCount: tracks.length + 1,
|
SingleChildScrollView(
|
||||||
itemBuilder: (context, index) {
|
child: Table(
|
||||||
if (index == 0) {
|
columnWidths: const {
|
||||||
return Column(
|
0: FixedColumnWidth(40),
|
||||||
children: [
|
1: FlexColumnWidth(),
|
||||||
TrackButton(
|
2: FlexColumnWidth(),
|
||||||
index: "#",
|
3: FixedColumnWidth(40),
|
||||||
trackName: "Title",
|
},
|
||||||
artists: ["Artist"],
|
children: [
|
||||||
album: "Album",
|
TableRow(
|
||||||
playback_time: "Time"),
|
children: [
|
||||||
const Divider()
|
TableCell(
|
||||||
],
|
child: Text(
|
||||||
);
|
"#",
|
||||||
}
|
textAlign: TextAlign.center,
|
||||||
Track track = tracks[index - 1];
|
style: tableHeadStyle,
|
||||||
return TrackButton(
|
)),
|
||||||
index: (index - 1).toString(),
|
TableCell(
|
||||||
thumbnail_url: track
|
child: Text(
|
||||||
.album?.images?.last.url ??
|
"Title",
|
||||||
"https://i.scdn.co/image/ab67616d00001e02b993cba8ff7d0a8e9ee18d46",
|
style: tableHeadStyle,
|
||||||
trackName: track.name!,
|
)),
|
||||||
artists: track.artists!
|
TableCell(
|
||||||
.map((e) => e.name!)
|
child: Text(
|
||||||
.toList(),
|
"Album",
|
||||||
album: track.album!.name!,
|
style: tableHeadStyle,
|
||||||
playback_time: track.duration!.inMinutes
|
)),
|
||||||
.toString(),
|
TableCell(
|
||||||
onTap: () {},
|
child: Text(
|
||||||
);
|
"Time",
|
||||||
}),
|
textAlign: TextAlign.center,
|
||||||
|
style: tableHeadStyle,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...trackToTableRow(tracks),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
46
lib/components/Settings.dart
Normal file
46
lib/components/Settings.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:spotube/provider/Auth.dart';
|
||||||
|
|
||||||
|
class Settings extends StatefulWidget {
|
||||||
|
const Settings({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_SettingsState createState() => _SettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsState extends State<Settings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: Theme.of(context).iconTheme,
|
||||||
|
title: const Text(
|
||||||
|
"Settings",
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
titleTextStyle: Theme.of(context).textTheme.headline4,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,77 +0,0 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class TrackButton extends StatefulWidget {
|
|
||||||
final String index;
|
|
||||||
final String trackName;
|
|
||||||
final List<String> artists;
|
|
||||||
final String album;
|
|
||||||
final String playback_time;
|
|
||||||
final String? thumbnail_url;
|
|
||||||
final void Function()? onTap;
|
|
||||||
TrackButton({
|
|
||||||
required this.index,
|
|
||||||
required this.trackName,
|
|
||||||
required this.artists,
|
|
||||||
required this.album,
|
|
||||||
required this.playback_time,
|
|
||||||
this.thumbnail_url,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_TrackButtonState createState() => _TrackButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TrackButtonState extends State<TrackButton> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
child: InkWell(
|
|
||||||
onTap: widget.onTap,
|
|
||||||
child: Ink(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.index,
|
|
||||||
style: const TextStyle(fontSize: 20),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
if (widget.thumbnail_url != null)
|
|
||||||
CachedNetworkImage(
|
|
||||||
imageUrl: widget.thumbnail_url!,
|
|
||||||
maxHeightDiskCache: 50,
|
|
||||||
maxWidthDiskCache: 50,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Container(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.trackName,
|
|
||||||
textAlign: TextAlign.justify,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold, fontSize: 17),
|
|
||||||
),
|
|
||||||
Text(widget.artists.join(", "))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Text(widget.album),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Text(widget.playback_time)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
3
lib/helpers/zero-pad-num-str.dart
Normal file
3
lib/helpers/zero-pad-num-str.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
String zeroPadNumStr(int input) {
|
||||||
|
return input < 10 ? "0$input" : input.toString();
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/components/Home.dart';
|
import 'package:spotube/components/Home.dart';
|
||||||
|
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||||
import 'package:spotube/provider/Auth.dart';
|
import 'package:spotube/provider/Auth.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';
|
||||||
@ -18,8 +20,36 @@ class MyApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider<Auth>(create: (context) => Auth()),
|
ChangeNotifierProvider<Auth>(create: (context) => Auth()),
|
||||||
ChangeNotifierProvider<SpotifyDI>(create: (context) {
|
ChangeNotifierProvider<SpotifyDI>(create: (context) {
|
||||||
Auth authState = Provider.of<Auth>(context, listen: false);
|
Auth authState = Provider.of<Auth>(context, listen: false);
|
||||||
return SpotifyDI(SpotifyApi(SpotifyApiCredentials(
|
return SpotifyDI(
|
||||||
authState.cliendId, authState.clientSecret)));
|
SpotifyApi(
|
||||||
|
SpotifyApiCredentials(
|
||||||
|
authState.clientId,
|
||||||
|
authState.clientSecret,
|
||||||
|
accessToken: authState.accessToken,
|
||||||
|
refreshToken: authState.refreshToken,
|
||||||
|
expiration: authState.expiration,
|
||||||
|
scopes: spotifyScopes,
|
||||||
|
),
|
||||||
|
onCredentialsRefreshed: (credentials) async {
|
||||||
|
SharedPreferences localStorage =
|
||||||
|
await SharedPreferences.getInstance();
|
||||||
|
localStorage.setString(
|
||||||
|
LocalStorageKeys.refreshToken,
|
||||||
|
credentials.refreshToken!,
|
||||||
|
);
|
||||||
|
localStorage.setString(
|
||||||
|
LocalStorageKeys.accessToken,
|
||||||
|
credentials.accessToken!,
|
||||||
|
);
|
||||||
|
localStorage.setString(
|
||||||
|
LocalStorageKeys.clientId, credentials.clientId!);
|
||||||
|
localStorage.setString(
|
||||||
|
LocalStorageKeys.clientSecret,
|
||||||
|
credentials.clientSecret!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
ChangeNotifierProvider<Playback>(create: (context) => Playback()),
|
ChangeNotifierProvider<Playback>(create: (context) => Playback()),
|
||||||
],
|
],
|
||||||
|
7
lib/models/LocalStorageKeys.dart
Normal file
7
lib/models/LocalStorageKeys.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
abstract class LocalStorageKeys {
|
||||||
|
static String clientId = 'client_id';
|
||||||
|
static String clientSecret = 'client_secret';
|
||||||
|
static String accessToken = 'access_token';
|
||||||
|
static String refreshToken = 'refresh_token';
|
||||||
|
static String expiration = " expiration";
|
||||||
|
}
|
@ -3,10 +3,17 @@ import 'package:flutter/cupertino.dart';
|
|||||||
class Auth with ChangeNotifier {
|
class Auth with ChangeNotifier {
|
||||||
String? _clientId;
|
String? _clientId;
|
||||||
String? _clientSecret;
|
String? _clientSecret;
|
||||||
|
String? _accessToken;
|
||||||
|
String? _refreshToken;
|
||||||
|
DateTime? _expiration;
|
||||||
|
|
||||||
bool _isLoggedIn = false;
|
bool _isLoggedIn = false;
|
||||||
|
|
||||||
String? get cliendId => _clientId;
|
String? get clientId => _clientId;
|
||||||
String? get clientSecret => _clientSecret;
|
String? get clientSecret => _clientSecret;
|
||||||
|
String? get accessToken => _accessToken;
|
||||||
|
String? get refreshToken => _refreshToken;
|
||||||
|
DateTime? get expiration => _expiration;
|
||||||
bool get isLoggedIn => _isLoggedIn;
|
bool get isLoggedIn => _isLoggedIn;
|
||||||
|
|
||||||
void setAuthState({
|
void setAuthState({
|
||||||
@ -14,17 +21,34 @@ class Auth with ChangeNotifier {
|
|||||||
bool safe = true,
|
bool safe = true,
|
||||||
String? clientId,
|
String? clientId,
|
||||||
String? clientSecret,
|
String? clientSecret,
|
||||||
String? refresh_token,
|
String? refreshToken,
|
||||||
String? access_token,
|
String? accessToken,
|
||||||
|
DateTime? expiration,
|
||||||
}) {
|
}) {
|
||||||
if (safe) {
|
if (safe) {
|
||||||
if (clientId != null) _clientId = clientId;
|
if (clientId != null) _clientId = clientId;
|
||||||
if (clientSecret != null) _clientSecret = clientSecret;
|
if (clientSecret != null) _clientSecret = clientSecret;
|
||||||
if (isLoggedIn != null) _isLoggedIn = isLoggedIn;
|
if (isLoggedIn != null) _isLoggedIn = isLoggedIn;
|
||||||
|
if (refreshToken != null) _refreshToken = refreshToken;
|
||||||
|
if (accessToken != null) _accessToken = accessToken;
|
||||||
|
if (expiration != null) _expiration = expiration;
|
||||||
} else {
|
} else {
|
||||||
_clientId = clientId;
|
_clientId = clientId;
|
||||||
_clientSecret = clientSecret;
|
_clientSecret = clientSecret;
|
||||||
|
_accessToken = accessToken;
|
||||||
|
_refreshToken = refreshToken;
|
||||||
|
_expiration = expiration;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
_clientId = null;
|
||||||
|
_clientSecret = null;
|
||||||
|
_accessToken = null;
|
||||||
|
_refreshToken = null;
|
||||||
|
_expiration = null;
|
||||||
|
_isLoggedIn = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@ flutter:
|
|||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
# assets:
|
assets:
|
||||||
# - images/a_dot_burr.jpeg
|
- assets/
|
||||||
# - images/a_dot_ham.jpeg
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
Loading…
Reference in New Issue
Block a user