anonymous (guest) login support added

build pipeline update to support anon login
not logged in guards added
This commit is contained in:
Kingkor Roy Tirtho 2022-03-19 14:34:39 +06:00
parent a06d891a04
commit 6f6c00d76d
16 changed files with 287 additions and 190 deletions

View File

@ -18,7 +18,7 @@ jobs:
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make libwebkit2gtk-4.0-dev keybinder-3.0 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make libwebkit2gtk-4.0-dev keybinder-3.0 python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse
- run: flutter config --enable-linux-desktop - run: flutter config --enable-linux-desktop
- run: flutter pub get - run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.SECRET }}' - run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter clean - run: flutter clean
- run: flutter build linux - run: flutter build linux
- run: make deb - run: make deb
@ -46,7 +46,7 @@ jobs:
cache: true cache: true
- run: flutter config --enable-windows-desktop - run: flutter config --enable-windows-desktop
- run: flutter pub get - run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.SECRET }}' - run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter build windows - run: flutter build windows
- run: choco install make -y - run: choco install make -y
- run: make innoinstall - run: make innoinstall
@ -67,7 +67,7 @@ jobs:
cache: true cache: true
- run: flutter config --enable-macos-desktop - run: flutter config --enable-macos-desktop
- run: flutter pub get - run: flutter pub get
- run: dart bin/create-secrets.dart '${{ secrets.SECRET }}' - run: dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
- run: flutter build macos - run: flutter build macos
- run: du -sh build/macos/Build/Products/Release/spotube.app - run: du -sh build/macos/Build/Products/Release/spotube.app
- run: npm install -g appdmg - run: npm install -g appdmg

View File

@ -5,17 +5,22 @@ import 'package:path/path.dart' as path;
void main(List<String> args) async { void main(List<String> args) async {
if (args.isEmpty) { if (args.isEmpty) {
throw ArgumentError("Expected an argument but none was passed"); throw ArgumentError(
"Expected 2 arguments but passed ${args.length < 2 || args.length > 2 ? args.length : "none"}");
} }
var decodedSecret = utf8.decode(base64Decode(args.first)); final decodedSecret = utf8.decode(base64Decode(args.first));
final decodedSpotifySecrete = utf8.decode(base64Decode(args.last));
final val = jsonDecode(decodedSecret); final val = jsonDecode(decodedSecret);
if (val is! List) { final val2 = jsonDecode(decodedSpotifySecrete);
if (val is! List || (val2 is! List && (val2 as List).first is! Map)) {
throw Exception( throw Exception(
"'SECRET' Environmental Variable isn't configured properly"); "'LYRICS_SECRET' and 'SPOTIFY_SECRET' Environmental Variable isn't configured properly");
} }
await File(path.join( await File(path.join(
Directory.current.path, "lib/models/generated_secrets.dart")) Directory.current.path, "lib/models/generated_secrets.dart"))
.writeAsString("final List<String> secrets = $decodedSecret;"); .writeAsString(
"final List<String> lyricsSecrets = $decodedSecret;\nfinal List<Map<String, dynamic>> spotifySecrets = $decodedSpotifySecrete;",
);
} }

View File

@ -11,18 +11,19 @@ import 'package:spotify/spotify.dart' hide Image, Player, Search;
import 'package:spotube/components/Category/CategoryCard.dart'; import 'package:spotube/components/Category/CategoryCard.dart';
import 'package:spotube/components/Home/Sidebar.dart'; import 'package:spotube/components/Home/Sidebar.dart';
import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; import 'package:spotube/components/Home/SpotubeNavigationBar.dart';
import 'package:spotube/components/Login.dart';
import 'package:spotube/components/Lyrics.dart'; import 'package:spotube/components/Lyrics.dart';
import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Search/Search.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Player/Player.dart';
import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/components/Library/UserLibrary.dart';
import 'package:spotube/helpers/get-random-element.dart';
import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/helpers/oauth-login.dart';
import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/useHotKeys.dart';
import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/usePagingController.dart';
import 'package:spotube/hooks/useSharedPreferences.dart'; import 'package:spotube/hooks/useSharedPreferences.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/generated_secrets.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';
@ -60,6 +61,32 @@ class Home extends HookConsumerWidget {
// initializing global hot keys // initializing global hot keys
useHotKeys(ref); useHotKeys(ref);
final listener = useCallback((int pageKey) async {
final spotify = ref.read(spotifyProvider);
try {
Page<Category> categories =
await spotify.categories.list(country: "US").getPage(15, pageKey);
final items = categories.items!.toList();
if (pageKey == 0) {
Category category = Category();
category.id = "user-featured-playlists";
category.name = "Featured";
items.insert(0, category);
}
if (categories.isLast && categories.items != null) {
pagingController.appendLastPage(items);
} else if (categories.items != null) {
pagingController.appendPage(items, categories.nextOffset);
}
} catch (e, stack) {
pagingController.error = e;
print("[Home.pagingController.addPageRequestListener] $e");
print(stack);
}
}, []);
useEffect(() { useEffect(() {
if (localStorage == null) return null; if (localStorage == null) return null;
final String? clientId = final String? clientId =
@ -72,48 +99,30 @@ class Home extends HookConsumerWidget {
localStorage.getString(LocalStorageKeys.refreshToken); localStorage.getString(LocalStorageKeys.refreshToken);
final String? expirationStr = final String? expirationStr =
localStorage.getString(LocalStorageKeys.expiration); localStorage.getString(LocalStorageKeys.expiration);
listener(pageKey) async {
final spotify = ref.read(spotifyProvider);
try {
Page<Category> categories =
await spotify.categories.list(country: "US").getPage(15, pageKey);
var items = categories.items!.toList();
if (pageKey == 0) {
Category category = Category();
category.id = "user-featured-playlists";
category.name = "Featured";
items.insert(0, category);
}
if (categories.isLast && categories.items != null) {
pagingController.appendLastPage(items);
} else if (categories.items != null) {
pagingController.appendPage(items, categories.nextOffset);
}
} catch (e, stack) {
pagingController.error = e;
print("[Home.pagingController.addPageRequestListener] $e");
print(stack);
}
}
try { try {
final DateTime? expiration = final DateTime? expiration =
expirationStr != null ? DateTime.parse(expirationStr) : null; expirationStr != null ? DateTime.parse(expirationStr) : null;
final anonCred = getRandomElement(spotifySecrets);
SpotifyApiCredentials apiCredentials =
clientId != null && clientSecret != null
? SpotifyApiCredentials(
clientId,
clientSecret,
accessToken: accessToken,
refreshToken: refreshToken,
expiration: expiration,
scopes: spotifyScopes,
)
: SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
);
SpotifyApi spotify = SpotifyApi(apiCredentials);
if (clientId != null && clientSecret != null) { if (clientId != null && clientSecret != null) {
SpotifyApi spotify = SpotifyApi(
SpotifyApiCredentials(
clientId,
clientSecret,
accessToken: accessToken,
refreshToken: refreshToken,
expiration: expiration,
scopes: spotifyScopes,
),
);
spotify.getCredentials().then((credentials) { spotify.getCredentials().then((credentials) {
if (credentials.accessToken?.isNotEmpty ?? false) { if (credentials.accessToken?.isNotEmpty == true) {
auth.setAuthState( auth.setAuthState(
clientId: clientId, clientId: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
@ -124,9 +133,11 @@ class Home extends HookConsumerWidget {
isLoggedIn: true, isLoggedIn: true,
); );
} }
return null;
}).then((_) {
pagingController.addPageRequestListener(listener); pagingController.addPageRequestListener(listener);
// the world is full of surprises and the previously working
// fine pageRequestListener now doesn't notify the listeners
// automatically after assigning a listener. So doing it manually
pagingController.notifyPageRequestListeners(0);
}).catchError((e, stack) { }).catchError((e, stack) {
if (e is AuthorizationException) { if (e is AuthorizationException) {
oauthLogin( oauthLogin(
@ -138,6 +149,9 @@ class Home extends HookConsumerWidget {
print("[Home.useEffect.spotify.getCredentials]: $e"); print("[Home.useEffect.spotify.getCredentials]: $e");
print(stack); print(stack);
}); });
} else {
pagingController.addPageRequestListener(listener);
pagingController.notifyPageRequestListeners(0);
} }
} catch (e, stack) { } catch (e, stack) {
print("[Home.initState]: $e"); print("[Home.initState]: $e");
@ -148,10 +162,6 @@ class Home extends HookConsumerWidget {
}; };
}, [localStorage]); }, [localStorage]);
if (!auth.isLoggedIn) {
return const Login();
}
return SafeArea( return SafeArea(
child: Scaffold( child: Scaffold(
body: Column( body: Column(

View File

@ -6,10 +6,9 @@ import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart' hide Image; import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/sideBarTiles.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import '../../models/sideBarTiles.dart';
class Sidebar extends HookConsumerWidget { class Sidebar extends HookConsumerWidget {
final int selectedIndex; final int selectedIndex;
final void Function(int) onSelectedIndexChanged; final void Function(int) onSelectedIndexChanged;
@ -98,7 +97,7 @@ class Sidebar extends HookConsumerWidget {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
snapshot.data?.displayName ?? "User's name", snapshot.data?.displayName ?? "Guest",
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),

View File

@ -1,11 +1,16 @@
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/provider/Auth.dart';
class UserLibrary extends StatelessWidget { class UserLibrary extends ConsumerWidget {
const UserLibrary({Key? key}) : super(key: key); const UserLibrary({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
final Auth auth = ref.watch(authProvider);
return Expanded( return Expanded(
child: DefaultTabController( child: DefaultTabController(
length: 3, length: 3,
@ -20,11 +25,13 @@ class UserLibrary extends StatelessWidget {
Tab(text: "Album"), Tab(text: "Album"),
], ],
), ),
body: const TabBarView(children: [ body: auth.isLoggedIn
UserPlaylists(), ? const TabBarView(children: [
UserArtists(), UserPlaylists(),
Icon(Icons.ac_unit_outlined), UserArtists(),
]), Icon(Icons.ac_unit_outlined),
])
: const AnonymousFallback(),
), ),
), ),
); );

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/helpers/oauth-login.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
@ -14,10 +16,12 @@ class Login extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
var clientIdController = useTextEditingController(); Auth authState = ref.watch(authProvider);
var clientSecretController = useTextEditingController(); final clientIdController = useTextEditingController();
var accessTokenController = useTextEditingController(); final clientSecretController = useTextEditingController();
var fieldError = useState(false); final accessTokenController = useTextEditingController();
final fieldError = useState(false);
final breakpoint = useBreakpoints();
Future handleLogin(Auth authState) async { Future handleLogin(Auth authState) async {
try { try {
@ -29,86 +33,94 @@ class Login extends HookConsumerWidget {
ref.read(authProvider), ref.read(authProvider),
clientId: clientIdController.value.text, clientId: clientIdController.value.text,
clientSecret: clientSecretController.value.text, clientSecret: clientSecretController.value.text,
).then(
(value) => GoRouter.of(context).pop(),
); );
} catch (e) { } catch (e) {
print("[Login.handleLogin] $e"); print("[Login.handleLogin] $e");
} }
} }
Auth authState = ref.watch(authProvider); final textTheme = Theme.of(context).textTheme;
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar(), appBar: const PageWindowTitleBar(leading: BackButton()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
child: Column( child: Container(
children: [ margin: const EdgeInsets.symmetric(horizontal: 10),
Image.asset( child: Column(
"assets/spotube-logo.png", children: [
width: 400, Image.asset(
height: 400, "assets/spotube-logo.png",
), width: MediaQuery.of(context).size.width *
Text("Add your spotify credentials to get started", (breakpoint <= Breakpoints.md ? .5 : .3),
style: Theme.of(context).textTheme.headline4),
const Text(
"Don't worry, any of your credentials won't be collected or shared with anyone"),
const Hyperlink("How to get these client-id & client-secret?",
"https://github.com/KRTirtho/spotube#configuration"),
const SizedBox(
height: 10,
),
Container(
constraints: const BoxConstraints(
maxWidth: 400,
), ),
child: Column( Text("Add your spotify credentials to get started",
children: [ style: breakpoint <= Breakpoints.md
TextField( ? textTheme.headline5
controller: clientIdController, : textTheme.headline4),
decoration: const InputDecoration( const Text(
hintText: "Spotify Client ID", "Don't worry, any of your credentials won't be collected or shared with anyone"),
label: Text("ClientID"), const Hyperlink("How to get these client-id & client-secret?",
), "https://github.com/KRTirtho/spotube#configuration"),
), const SizedBox(
const SizedBox(height: 10), height: 10,
TextField(
decoration: const InputDecoration(
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
),
controller: clientSecretController,
),
const SizedBox(height: 10),
const Divider(color: Colors.grey),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
label: Text("Genius Access Token (optional)"),
),
controller: accessTokenController,
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () async {
await handleLogin(authState);
UserPreferences preferences =
ref.read(userPreferencesProvider);
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(
accessTokenController.value.text);
await localStorage.setString(
LocalStorageKeys.geniusAccessToken,
accessTokenController.value.text);
accessTokenController.text = "";
},
child: const Text("Submit"),
)
],
), ),
), Container(
], constraints: const BoxConstraints(
maxWidth: 400,
),
child: Column(
children: [
TextField(
controller: clientIdController,
decoration: const InputDecoration(
hintText: "Spotify Client ID",
label: Text("ClientID"),
),
),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
),
controller: clientSecretController,
),
const SizedBox(height: 10),
const Divider(color: Colors.grey),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
label: Text("Genius Access Token (optional)"),
),
controller: accessTokenController,
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () async {
await handleLogin(authState);
UserPreferences preferences =
ref.read(userPreferencesProvider);
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(
accessTokenController.value.text);
await localStorage.setString(
LocalStorageKeys.geniusAccessToken,
accessTokenController.value.text);
accessTokenController.text = "";
},
child: const Text("Submit"),
)
],
),
),
],
),
), ),
), ),
), ),

View File

@ -4,18 +4,15 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart' hide Image;
import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerActions.dart';
import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart';
import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart';
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerControls.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.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:spotube/provider/SpotifyDI.dart';
class Player extends HookConsumerWidget { class Player extends HookConsumerWidget {
const Player({Key? key}) : super(key: key); const Player({Key? key}) : super(key: key);

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.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';
@ -16,32 +17,35 @@ class PlayerActions extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final SpotifyApi spotifyApi = ref.watch(spotifyProvider); final SpotifyApi spotifyApi = ref.watch(spotifyProvider);
final Playback playback = ref.watch(playbackProvider); final Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider);
return Row( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ children: [
DownloadTrackButton( DownloadTrackButton(
track: playback.currentTrack, track: playback.currentTrack,
), ),
FutureBuilder<bool>( if (auth.isLoggedIn)
future: playback.currentTrack?.id != null FutureBuilder<bool>(
? spotifyApi.tracks.me.containsOne(playback.currentTrack!.id!) future: playback.currentTrack?.id != null
: Future.value(false), ? spotifyApi.tracks.me.containsOne(playback.currentTrack!.id!)
initialData: false, : Future.value(false),
builder: (context, snapshot) { initialData: false,
bool isLiked = snapshot.data ?? false; builder: (context, snapshot) {
return IconButton( bool isLiked = snapshot.data ?? false;
icon: Icon( return IconButton(
!isLiked icon: Icon(
? Icons.favorite_outline_rounded !isLiked
: Icons.favorite_rounded, ? Icons.favorite_outline_rounded
color: isLiked ? Colors.green : null, : Icons.favorite_rounded,
), color: isLiked ? Colors.green : null,
onPressed: () { ),
if (!isLiked && playback.currentTrack?.id != null) { onPressed: () {
spotifyApi.tracks.me.saveOne(playback.currentTrack!.id!); if (!isLiked && playback.currentTrack?.id != null) {
} spotifyApi.tracks.me
}); .saveOne(playback.currentTrack!.id!);
}), }
});
}),
], ],
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/provider/Auth.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:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -35,6 +36,7 @@ class PlaylistView extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
final Auth auth = ref.watch(authProvider);
SpotifyApi spotifyApi = ref.watch(spotifyProvider); SpotifyApi spotifyApi = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null && var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == playlist.id; playback.currentPlaylist?.id == playlist.id;
@ -56,10 +58,11 @@ class PlaylistView extends ConsumerWidget {
// nav back // nav back
const BackButton(), const BackButton(),
// heart playlist // heart playlist
IconButton( if (auth.isLoggedIn)
icon: const Icon(Icons.favorite_outline_rounded), IconButton(
onPressed: () {}, icon: const Icon(Icons.favorite_outline_rounded),
), onPressed: () {},
),
// play playlist // play playlist
IconButton( IconButton(
icon: Icon( icon: Icon(

View File

@ -5,11 +5,13 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Shared/AnonymousFallback.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-album-to-album.dart'; import 'package:spotube/helpers/simple-album-to-album.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useBreakpoints.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,7 +20,8 @@ class Search extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
SpotifyApi spotify = ref.watch(spotifyProvider); final SpotifyApi spotify = ref.watch(spotifyProvider);
final Auth auth = ref.watch(authProvider);
var controller = useTextEditingController(); var controller = useTextEditingController();
var searchTerm = useState(""); var searchTerm = useState("");
final albumController = useScrollController(); final albumController = useScrollController();
@ -26,6 +29,10 @@ class Search extends HookConsumerWidget {
final artistController = useScrollController(); final artistController = useScrollController();
final breakpoint = useBreakpoints(); final breakpoint = useBreakpoints();
if (auth.isAnonymous) {
return const Expanded(child: AnonymousFallback());
}
return Expanded( return Expanded(
child: Column( child: Column(
children: [ children: [

View File

@ -18,8 +18,9 @@ class Settings extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
UserPreferences preferences = ref.watch(userPreferencesProvider); final UserPreferences preferences = ref.watch(userPreferencesProvider);
ThemeMode theme = ref.watch(themeProvider); final ThemeMode theme = ref.watch(themeProvider);
final Auth auth = ref.watch(authProvider);
var geniusAccessToken = useState<String?>(null); var geniusAccessToken = useState<String?>(null);
TextEditingController textEditingController = useTextEditingController(); TextEditingController textEditingController = useTextEditingController();
@ -138,28 +139,50 @@ class Settings extends HookConsumerWidget {
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Builder(builder: (context) { if (auth.isAnonymous)
Auth auth = ref.watch(authProvider); Row(
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text("Log out of this account"), const Text("Login with your Spotify"),
ElevatedButton( ElevatedButton(
child: const Text("Logout"), child: Text("Connect with Spotify".toUpperCase()),
style: ButtonStyle( onPressed: () {
backgroundColor: MaterialStateProperty.all(Colors.red), GoRouter.of(context).push("/login");
),
onPressed: () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
await localStorage.clear();
auth.logout();
GoRouter.of(context).pop();
}, },
), style: ButtonStyle(
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25.0),
),
),
),
)
], ],
); ),
}), if (auth.isLoggedIn)
Builder(builder: (context) {
Auth auth = ref.watch(authProvider);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Log out of this account"),
ElevatedButton(
child: const Text("Logout"),
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.red),
),
onPressed: () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
await localStorage.clear();
auth.logout();
GoRouter.of(context).pop();
},
),
],
);
}),
const SizedBox(height: 40), const SizedBox(height: 40),
const Text("Spotube v1.2.0"), const Text("Spotube v1.2.0"),
const SizedBox(height: 10), const SizedBox(height: 10),

View File

@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AnonymousFallback extends StatelessWidget {
const AnonymousFallback({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(child: Text("You're not logged in"));
}
}

View File

@ -54,7 +54,9 @@ Future<List?> searchSong(
bool authHeader = false, bool authHeader = false,
}) async { }) async {
try { try {
if (apiKey == "" || apiKey == null) apiKey = getRandomElement(secrets); if (apiKey == "" || apiKey == null) {
apiKey = getRandomElement(lyricsSecrets);
}
const searchUrl = 'https://api.genius.com/search?q='; const searchUrl = 'https://api.genius.com/search?q=';
String song = optimizeQuery ? getTitle(title, artist) : "$title $artist"; String song = optimizeQuery ? getTitle(title, artist) : "$title $artist";

View File

@ -5,6 +5,7 @@ import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart';
import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart';
import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/components/Login.dart';
import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Player/PlayerView.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Settings.dart';
@ -16,6 +17,12 @@ GoRouter createGoRouter() => GoRouter(
path: "/", path: "/",
builder: (context, state) => const Home(), builder: (context, state) => const Home(),
), ),
GoRoute(
path: "/login",
pageBuilder: (context, state) => SpotubePage(
child: const Login(),
),
),
GoRoute( GoRoute(
path: "/settings", path: "/settings",
pageBuilder: (context, state) => SpotubePage( pageBuilder: (context, state) => SpotubePage(

View File

@ -16,6 +16,8 @@ class Auth with ChangeNotifier {
String? get refreshToken => _refreshToken; String? get refreshToken => _refreshToken;
DateTime? get expiration => _expiration; DateTime? get expiration => _expiration;
bool get isLoggedIn => _isLoggedIn; bool get isLoggedIn => _isLoggedIn;
bool get isAnonymous =>
!_isLoggedIn && _clientId == null && _clientSecret == null;
void setAuthState({ void setAuthState({
bool? isLoggedIn, bool? isLoggedIn,

View File

@ -2,21 +2,30 @@ import 'package:flutter_riverpod/flutter_riverpod.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';
import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/components/Home/Home.dart';
import 'package:spotube/helpers/get-random-element.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
var spotifyProvider = Provider<SpotifyApi>((ref) { var spotifyProvider = Provider<SpotifyApi>((ref) {
Auth authState = ref.watch(authProvider); Auth authState = ref.watch(authProvider);
final anonCred = getRandomElement(spotifySecrets);
SpotifyApiCredentials apiCredentials = authState.isAnonymous
? SpotifyApiCredentials(
anonCred["clientId"],
anonCred["clientSecret"],
)
: SpotifyApiCredentials(
authState.clientId,
authState.clientSecret,
accessToken: authState.accessToken,
refreshToken: authState.refreshToken,
expiration: authState.expiration,
scopes: spotifyScopes,
);
return SpotifyApi( return SpotifyApi(
SpotifyApiCredentials( apiCredentials,
authState.clientId,
authState.clientSecret,
accessToken: authState.accessToken,
refreshToken: authState.refreshToken,
expiration: authState.expiration,
scopes: spotifyScopes,
),
onCredentialsRefreshed: (credentials) async { onCredentialsRefreshed: (credentials) async {
SharedPreferences localStorage = await SharedPreferences.getInstance(); SharedPreferences localStorage = await SharedPreferences.getInstance();
localStorage.setString( localStorage.setString(