hook support added to most of the components

This commit is contained in:
Kingkor Roy Tirtho 2022-02-11 19:29:31 +06:00
parent d05ec0099d
commit 9fc155c000
19 changed files with 297 additions and 421 deletions

View File

@ -14,24 +14,19 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class ArtistProfile extends ConsumerStatefulWidget {
class ArtistProfile extends ConsumerWidget {
final String artistId;
const ArtistProfile(this.artistId, {Key? key}) : super(key: key);
@override
_ArtistProfileState createState() => _ArtistProfileState();
}
class _ArtistProfileState extends ConsumerState<ArtistProfile> {
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
SpotifyApi spotify = ref.watch(spotifyProvider);
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
),
body: FutureBuilder<Artist>(
future: spotify.artists.get(widget.artistId),
future: spotify.artists.get(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
@ -222,7 +217,7 @@ class _ArtistProfileState extends ConsumerState<ArtistProfile> {
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ArtistAlbumView(
widget.artistId,
artistId,
snapshot.data?.name ?? "KRTX",
),
));
@ -260,7 +255,7 @@ class _ArtistProfileState extends ConsumerState<ArtistProfile> {
),
const SizedBox(height: 10),
FutureBuilder<Iterable<Artist>>(
future: spotify.artists.getRelatedArtists(widget.artistId),
future: spotify.artists.getRelatedArtists(artistId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(

View File

@ -5,7 +5,7 @@ import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Playlist/PlaylistGenreView.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget {
class CategoryCard extends StatelessWidget {
final Category category;
final Iterable<PlaylistSimple>? playlists;
const CategoryCard(
@ -14,11 +14,6 @@ class CategoryCard extends StatefulWidget {
this.playlists,
}) : super(key: key);
@override
_CategoryCardState createState() => _CategoryCardState();
}
class _CategoryCardState extends State<CategoryCard> {
@override
Widget build(BuildContext context) {
return Column(
@ -29,7 +24,7 @@ class _CategoryCardState extends State<CategoryCard> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.category.name ?? "Unknown",
category.name ?? "Unknown",
style: Theme.of(context).textTheme.headline5,
),
TextButton(
@ -38,9 +33,9 @@ class _CategoryCardState extends State<CategoryCard> {
MaterialPageRoute(
builder: (context) {
return PlaylistGenreView(
widget.category.id!,
widget.category.name!,
playlists: widget.playlists,
category.id!,
category.name!,
playlists: playlists,
);
},
),
@ -55,14 +50,13 @@ class _CategoryCardState extends State<CategoryCard> {
builder: (context, ref, child) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null
? (widget.category.id != "user-featured-playlists"
? spotifyApi.playlists
.getByCategoryId(widget.category.id!)
future: playlists == null
? (category.id != "user-featured-playlists"
? spotifyApi.playlists.getByCategoryId(category.id!)
: spotifyApi.playlists.featured)
.getPage(4, 0)
.then((value) => value.items ?? [])
: Future.value(widget.playlists),
: Future.value(playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));

View File

@ -2,14 +2,8 @@ import 'package:flutter/material.dart' hide Image;
import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserPlaylists.dart';
class UserLibrary extends StatefulWidget {
class UserLibrary extends StatelessWidget {
const UserLibrary({Key? key}) : super(key: key);
@override
_UserLibraryState createState() => _UserLibraryState();
}
class _UserLibraryState extends State<UserLibrary> {
@override
Widget build(BuildContext context) {
return Expanded(

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -8,35 +9,32 @@ import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart';
class Login extends ConsumerStatefulWidget {
class Login extends HookConsumerWidget {
const Login({Key? key}) : super(key: key);
@override
_LoginState createState() => _LoginState();
}
Widget build(BuildContext context, ref) {
var clientIdController = useTextEditingController();
var clientSecretController = useTextEditingController();
var accessTokenController = useTextEditingController();
var fieldError = useState(false);
class _LoginState extends ConsumerState<Login> {
String clientId = "";
String clientSecret = "";
String accessToken = "";
bool _fieldError = false;
Future handleLogin(Auth authState) async {
try {
if (clientId == "" || clientSecret == "") {
return setState(() {
_fieldError = true;
});
Future handleLogin(Auth authState) async {
try {
if (clientIdController.value.text == "" ||
clientSecretController.value.text == "") {
fieldError.value = true;
}
await oauthLogin(
ref.read(authProvider),
clientId: clientIdController.value.text,
clientSecret: clientSecretController.value.text,
);
} catch (e) {
print("[Login.handleLogin] $e");
}
await oauthLogin(ref.read(authProvider),
clientId: clientId, clientSecret: clientSecret);
} catch (e) {
print("[Login.handleLogin] $e");
}
}
@override
Widget build(BuildContext context) {
Auth authState = ref.watch(authProvider);
return Scaffold(
appBar: const PageWindowTitleBar(),
@ -65,15 +63,11 @@ class _LoginState extends ConsumerState<Login> {
child: Column(
children: [
TextField(
controller: clientIdController,
decoration: const InputDecoration(
hintText: "Spotify Client ID",
label: Text("ClientID"),
),
onChanged: (value) {
setState(() {
clientId = value;
});
},
),
const SizedBox(height: 10),
TextField(
@ -81,11 +75,7 @@ class _LoginState extends ConsumerState<Login> {
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
),
onChanged: (value) {
setState(() {
clientSecret = value;
});
},
controller: clientSecretController,
),
const SizedBox(height: 10),
const Divider(color: Colors.grey),
@ -94,11 +84,7 @@ class _LoginState extends ConsumerState<Login> {
decoration: const InputDecoration(
label: Text("Genius Access Token (optional)"),
),
onChanged: (value) {
setState(() {
accessToken = value;
});
},
controller: accessTokenController,
),
const SizedBox(
height: 10,
@ -110,12 +96,12 @@ class _LoginState extends ConsumerState<Login> {
ref.read(userPreferencesProvider);
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(accessToken);
preferences.setGeniusAccessToken(
accessTokenController.value.text);
await localStorage.setString(
LocalStorageKeys.geniusAccessToken, accessToken);
setState(() {
accessToken = "";
});
LocalStorageKeys.geniusAccessToken,
accessTokenController.value.text);
accessTokenController.text = "";
},
child: const Text("Submit"),
)

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Settings.dart';
import 'package:spotube/helpers/artist-to-string.dart';
@ -7,48 +8,53 @@ import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart';
class Lyrics extends ConsumerStatefulWidget {
class Lyrics extends HookConsumerWidget {
const Lyrics({Key? key}) : super(key: key);
@override
ConsumerState<Lyrics> createState() => _LyricsState();
}
class _LyricsState extends ConsumerState<Lyrics> {
Map<String, String> _lyrics = {};
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
UserPreferences userPreferences = ref.watch(userPreferencesProvider);
var lyrics = useState({});
bool hasToken = (userPreferences.geniusAccessToken != null ||
(userPreferences.geniusAccessToken?.isNotEmpty ?? false));
if (playback.currentTrack != null &&
hasToken &&
playback.currentTrack!.id != _lyrics["id"]) {
getLyrics(
var lyricsFuture = useMemoized(() {
if (playback.currentTrack == null ||
!hasToken ||
(playback.currentTrack?.id != null &&
playback.currentTrack?.id == lyrics.value["id"])) {
return null;
}
return getLyrics(
playback.currentTrack!.name!,
artistsToString<Artist>(playback.currentTrack!.artists ?? []),
apiKey: userPreferences.geniusAccessToken!,
optimizeQuery: true,
).then((lyrics) {
if (lyrics != null) {
setState(() {
_lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!};
});
}
});
}
);
}, [playback.currentTrack]);
if (_lyrics["lyrics"] != null && playback.currentTrack == null) {
setState(() {
_lyrics = {};
});
}
var lyricsSnapshot = useFuture(lyricsFuture);
if (_lyrics["lyrics"] == null && playback.currentTrack != null) {
useEffect(() {
if (lyricsSnapshot.hasData && lyricsSnapshot.data != null) {
lyrics.value = {
"lyrics": lyricsSnapshot.data,
"id": playback.currentTrack!.id!
};
}
if (lyrics.value["lyrics"] != null && playback.currentTrack == null) {
lyrics.value = {};
}
}, [
lyricsSnapshot.data,
lyricsSnapshot.hasData,
lyrics.value,
playback.currentTrack,
]);
if (lyrics.value["lyrics"] == null && playback.currentTrack != null) {
if (!hasToken) {
return Expanded(
child: Column(
@ -99,9 +105,10 @@ class _LyricsState extends ConsumerState<Lyrics> {
child: SingleChildScrollView(
child: Center(
child: Text(
_lyrics["lyrics"] == null && playback.currentTrack == null
lyrics.value["lyrics"] == null &&
playback.currentTrack == null
? "No Track being played currently"
: _lyrics["lyrics"]!,
: lyrics.value["lyrics"]!,
style: Theme.of(context).textTheme.headline6,
),
),

View File

@ -176,7 +176,6 @@ class _PlayerState extends ConsumerState<Player> with WidgetsBindingObserver {
await player.pause();
await player.seek(Duration.zero);
_movePlaylistPositionBy(1);
print("ON NEXT");
} catch (e, stack) {
print("[PlayerControls.onNext()] $e");
print(stack);

View File

@ -1,13 +1,14 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/models/GlobalKeyActions.dart';
import 'package:spotube/provider/UserPreferences.dart';
class PlayerControls extends ConsumerStatefulWidget {
class PlayerControls extends HookConsumerWidget {
final Stream<Duration> positionStream;
final bool isPlaying;
final Duration duration;
@ -34,33 +35,21 @@ class PlayerControls extends ConsumerStatefulWidget {
Key? key,
}) : super(key: key);
@override
_PlayerControlsState createState() => _PlayerControlsState();
}
class _PlayerControlsState extends ConsumerState<PlayerControls> {
StreamSubscription? _timePositionListener;
late List<GlobalKeyActions> _hotKeys = [];
@override
void dispose() async {
await _timePositionListener?.cancel();
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
super.dispose();
}
_playOrPause(key) async {
try {
widget.isPlaying ? widget.onPause?.call() : await widget.onPlay?.call();
isPlaying ? await onPause?.call() : await onPlay?.call();
} catch (e, stack) {
print("[PlayPauseShortcut] $e");
print(stack);
}
}
_configureHotKeys(UserPreferences preferences) async {
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)))
.then((val) async {
@override
Widget build(BuildContext context, ref) {
UserPreferences preferences = ref.watch(userPreferencesProvider);
var _hotKeys = [];
useEffect(() {
_hotKeys = [
GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
@ -68,14 +57,14 @@ class _PlayerControlsState extends ConsumerState<PlayerControls> {
),
if (preferences.nextTrackHotKey != null)
GlobalKeyActions(
preferences.nextTrackHotKey!, (key) => widget.onNext?.call()),
preferences.nextTrackHotKey!, (key) => onNext?.call()),
if (preferences.prevTrackHotKey != null)
GlobalKeyActions(
preferences.prevTrackHotKey!, (key) => widget.onPrevious?.call()),
preferences.prevTrackHotKey!, (key) => onPrevious?.call()),
if (preferences.playPauseHotKey != null)
GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause)
];
await Future.wait(
Future.wait(
_hotKeys.map((e) {
return hotKeyManager.register(
e.hotKey,
@ -83,25 +72,22 @@ class _PlayerControlsState extends ConsumerState<PlayerControls> {
);
}),
);
return () {
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
};
});
}
@override
Widget build(BuildContext context) {
UserPreferences preferences = ref.watch(userPreferencesProvider);
_configureHotKeys(preferences);
return Container(
constraints: const BoxConstraints(maxWidth: 700),
child: Column(
children: [
StreamBuilder<Duration>(
stream: widget.positionStream,
stream: positionStream,
builder: (context, snapshot) {
var totalMinutes =
zeroPadNumStr(widget.duration.inMinutes.remainder(60));
zeroPadNumStr(duration.inMinutes.remainder(60));
var totalSeconds =
zeroPadNumStr(widget.duration.inSeconds.remainder(60));
zeroPadNumStr(duration.inSeconds.remainder(60));
var currentMinutes = snapshot.hasData
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
: "00";
@ -109,7 +95,7 @@ class _PlayerControlsState extends ConsumerState<PlayerControls> {
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
: "00";
var sliderMax = widget.duration.inSeconds;
var sliderMax = duration.inSeconds;
var sliderValue = snapshot.data?.inSeconds ?? 0;
return Row(
children: [
@ -123,7 +109,7 @@ class _PlayerControlsState extends ConsumerState<PlayerControls> {
: sliderValue / sliderMax,
onChanged: (value) {},
onChangeEnd: (value) {
widget.onSeek?.call(value * sliderMax);
onSeek?.call(value * sliderMax);
},
),
),
@ -138,30 +124,27 @@ class _PlayerControlsState extends ConsumerState<PlayerControls> {
children: [
IconButton(
icon: const Icon(Icons.shuffle_rounded),
color:
widget.shuffled ? Theme.of(context).primaryColor : null,
color: shuffled ? Theme.of(context).primaryColor : null,
onPressed: () {
widget.onShuffle?.call();
onShuffle?.call();
}),
IconButton(
icon: const Icon(Icons.skip_previous_rounded),
onPressed: () {
widget.onPrevious?.call();
onPrevious?.call();
}),
IconButton(
icon: Icon(
widget.isPlaying
? Icons.pause_rounded
: Icons.play_arrow_rounded,
isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
),
onPressed: () => _playOrPause(null),
),
IconButton(
icon: const Icon(Icons.skip_next_rounded),
onPressed: () => widget.onNext?.call()),
onPressed: () => onNext?.call()),
IconButton(
icon: const Icon(Icons.stop_rounded),
onPressed: () => widget.onStop?.call(),
onPressed: () => onStop?.call(),
)
],
)

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
@ -7,27 +7,22 @@ import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistCard extends ConsumerStatefulWidget {
class PlaylistCard extends ConsumerWidget {
final PlaylistSimple playlist;
const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistCardState createState() => _PlaylistCardState();
}
class _PlaylistCardState extends ConsumerState<PlaylistCard> {
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null &&
playback.currentPlaylist!.id == widget.playlist.id;
playback.currentPlaylist!.id == playlist.id;
return PlaybuttonCard(
title: widget.playlist.name!,
imageUrl: widget.playlist.images![0].url!,
title: playlist.name!,
imageUrl: playlist.images![0].url!,
isPlaying: isPlaylistPlaying,
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return PlaylistView(widget.playlist);
return PlaylistView(playlist);
},
));
},
@ -35,9 +30,9 @@ class _PlaylistCardState extends ConsumerState<PlaylistCard> {
if (isPlaylistPlaying) return;
SpotifyApi spotifyApi = ref.read(spotifyProvider);
List<Track> tracks = (widget.playlist.id != "user-liked-tracks"
List<Track> tracks = (playlist.id != "user-liked-tracks"
? await spotifyApi.playlists
.getTracksByPlaylistId(widget.playlist.id!)
.getTracksByPlaylistId(playlist.id!)
.all()
: await spotifyApi.tracks.me.saved
.all()
@ -48,9 +43,9 @@ class _PlaylistCardState extends ConsumerState<PlaylistCard> {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: widget.playlist.id!,
name: widget.playlist.name!,
thumbnail: imageToUrlString(widget.playlist.images),
id: playlist.id!,
name: playlist.name!,
thumbnail: imageToUrlString(playlist.images),
);
playback.setCurrentTrack = tracks.first;
},

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends StatefulWidget {
class PlaylistGenreView extends ConsumerWidget {
final String genreId;
final String genreName;
final Iterable<PlaylistSimple>? playlists;
@ -15,13 +15,9 @@ class PlaylistGenreView extends StatefulWidget {
this.playlists,
Key? key,
}) : super(key: key);
@override
_PlaylistGenreViewState createState() => _PlaylistGenreViewState();
}
class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
@ -29,7 +25,7 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
body: Column(
children: [
Text(
widget.genreName,
genreName,
style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center,
),
@ -39,13 +35,13 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
return Expanded(
child: SingleChildScrollView(
child: FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null
? (widget.genreId != "user-featured-playlists"
future: playlists == null
? (genreId != "user-featured-playlists"
? spotifyApi.playlists
.getByCategoryId(widget.genreId)
.getByCategoryId(genreId)
.all()
: spotifyApi.playlists.featured.all())
: Future.value(widget.playlists),
: Future.value(playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));

View File

@ -7,24 +7,20 @@ import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistView extends ConsumerStatefulWidget {
class PlaylistView extends ConsumerWidget {
final PlaylistSimple playlist;
const PlaylistView(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistViewState createState() => _PlaylistViewState();
}
class _PlaylistViewState extends ConsumerState<PlaylistView> {
playPlaylist(Playback playback, List<Track> tracks, {Track? currentTrack}) {
currentTrack ??= tracks.first;
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == widget.playlist.id;
playback.currentPlaylist?.id == playlist.id;
if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks,
id: widget.playlist.id!,
name: widget.playlist.name!,
thumbnail: imageToUrlString(widget.playlist.images),
id: playlist.id!,
name: playlist.name!,
thumbnail: imageToUrlString(playlist.images),
);
playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying &&
@ -35,17 +31,15 @@ class _PlaylistViewState extends ConsumerState<PlaylistView> {
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
Playback playback = ref.watch(playbackProvider);
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == widget.playlist.id;
playback.currentPlaylist?.id == playlist.id;
return Scaffold(
body: FutureBuilder<Iterable<Track>>(
future: widget.playlist.id != "user-liked-tracks"
? spotifyApi.playlists
.getTracksByPlaylistId(widget.playlist.id)
.all()
future: playlist.id != "user-liked-tracks"
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
: spotifyApi.tracks.me.saved
.all()
.then((tracks) => tracks.map((e) => e.track!)),
@ -78,7 +72,7 @@ class _PlaylistViewState extends ConsumerState<PlaylistView> {
),
),
Center(
child: Text(widget.playlist.name!,
child: Text(playlist.name!,
style: Theme.of(context).textTheme.headline4),
),
snapshot.hasError

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistCard.dart';
@ -11,27 +12,14 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class Search extends ConsumerStatefulWidget {
class Search extends HookConsumerWidget {
const Search({Key? key}) : super(key: key);
@override
ConsumerState<Search> createState() => _SearchState();
}
class _SearchState extends ConsumerState<Search> {
late TextEditingController _controller;
String searchTerm = "";
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
SpotifyApi spotify = ref.watch(spotifyProvider);
var controller = useTextEditingController();
var searchTerm = useState("");
return Expanded(
child: Column(
@ -43,11 +31,9 @@ class _SearchState extends ConsumerState<Search> {
Expanded(
child: TextField(
decoration: const InputDecoration(hintText: "Search..."),
controller: _controller,
controller: controller,
onSubmitted: (value) {
setState(() {
searchTerm = _controller.value.text;
});
searchTerm.value = controller.value.text;
},
),
),
@ -60,24 +46,22 @@ class _SearchState extends ConsumerState<Search> {
textColor: Colors.white,
child: const Icon(Icons.search_rounded),
onPressed: () {
setState(() {
searchTerm = _controller.value.text;
});
searchTerm.value = controller.value.text;
},
),
],
),
),
FutureBuilder<List<Page>>(
future: searchTerm.isNotEmpty
? spotify.search.get(searchTerm).first(10)
future: searchTerm.value.isNotEmpty
? spotify.search.get(searchTerm.value).first(10)
: null,
builder: (context, snapshot) {
if (!snapshot.hasData && searchTerm.isNotEmpty) {
if (!snapshot.hasData && searchTerm.value.isNotEmpty) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
} else if (!snapshot.hasData && searchTerm.isEmpty) {
} else if (!snapshot.hasData && searchTerm.value.isEmpty) {
return Container();
}
Playback playback = ref.watch(playbackProvider);

View File

@ -1,45 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/main.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/ThemeProvider.dart';
import 'package:spotube/provider/UserPreferences.dart';
class Settings extends ConsumerStatefulWidget {
class Settings extends HookConsumerWidget {
const Settings({Key? key}) : super(key: key);
@override
_SettingsState createState() => _SettingsState();
}
class _SettingsState extends ConsumerState<Settings> {
TextEditingController? _textEditingController;
String? _geniusAccessToken;
@override
void initState() {
super.initState();
_textEditingController = TextEditingController();
_textEditingController?.addListener(() {
setState(() {
_geniusAccessToken = _textEditingController?.value.text;
});
});
}
@override
void dispose() {
_textEditingController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, ref) {
UserPreferences preferences = ref.watch(userPreferencesProvider);
ThemeMode theme = ref.watch(themeProvider);
var geniusAccessToken = useState<String?>(null);
TextEditingController textEditingController = useTextEditingController();
textEditingController.addListener(() {
geniusAccessToken.value = textEditingController.value.text;
});
return Scaffold(
appBar: PageWindowTitleBar(
@ -65,7 +48,7 @@ class _SettingsState extends ConsumerState<Settings> {
Expanded(
flex: 1,
child: TextField(
controller: _textEditingController,
controller: textEditingController,
decoration: InputDecoration(
hintText: preferences.geniusAccessToken,
),
@ -74,19 +57,19 @@ class _SettingsState extends ConsumerState<Settings> {
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: _geniusAccessToken != null
onPressed: geniusAccessToken != null
? () async {
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences
.setGeniusAccessToken(_geniusAccessToken);
.setGeniusAccessToken(geniusAccessToken.value);
localStorage.setString(
LocalStorageKeys.geniusAccessToken,
_geniusAccessToken!);
setState(() {
_geniusAccessToken = null;
});
_textEditingController?.text = "";
geniusAccessToken.value ?? "");
geniusAccessToken.value = null;
textEditingController.text = "";
}
: null,
child: const Text("Save"),
@ -121,7 +104,7 @@ class _SettingsState extends ConsumerState<Settings> {
children: [
const Text("Theme"),
DropdownButton<ThemeMode>(
value: MyApp.of(context)?.getThemeMode(),
value: theme,
items: const [
DropdownMenuItem(
child: Text(
@ -142,7 +125,7 @@ class _SettingsState extends ConsumerState<Settings> {
],
onChanged: (value) {
if (value != null) {
MyApp.of(context)?.setThemeMode(value);
ref.read(themeProvider.notifier).state = value;
}
},
)

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class AnchorButton<T> extends StatefulWidget {
class AnchorButton<T> extends HookWidget {
final String text;
final TextStyle style;
final TextAlign? textAlign;
@ -16,33 +17,29 @@ class AnchorButton<T> extends StatefulWidget {
this.style = const TextStyle(),
}) : super(key: key);
@override
State<AnchorButton<T>> createState() => _AnchorButtonState<T>();
}
class _AnchorButtonState<T> extends State<AnchorButton<T>> {
bool _hover = false;
bool _tap = false;
@override
Widget build(BuildContext context) {
var hover = useState(false);
var tap = useState(false);
return GestureDetector(
child: MouseRegion(
cursor: MaterialStateMouseCursor.clickable,
child: Text(
widget.text,
style: widget.style.copyWith(
decoration: _hover || _tap ? TextDecoration.underline : null,
text,
style: style.copyWith(
decoration:
hover.value || tap.value ? TextDecoration.underline : null,
),
textAlign: widget.textAlign,
overflow: widget.overflow,
textAlign: textAlign,
overflow: overflow,
),
onEnter: (event) => setState(() => _hover = true),
onExit: (event) => setState(() => _hover = false),
onEnter: (event) => hover.value = true,
onExit: (event) => hover.value = false,
),
onTapDown: (event) => setState(() => _tap = true),
onTapUp: (event) => setState(() => _tap = false),
onTap: widget.onTap,
onTapDown: (event) => tap.value = true,
onTapUp: (event) => tap.value = false,
onTap: onTap,
);
}
}

View File

@ -1,106 +1,86 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path;
class DownloadTrackButton extends StatefulWidget {
enum TrackStatus { downloading, idle, done }
class DownloadTrackButton extends HookWidget {
final Track? track;
const DownloadTrackButton({Key? key, this.track}) : super(key: key);
@override
_DownloadTrackButtonState createState() => _DownloadTrackButtonState();
}
Widget build(BuildContext context) {
var status = useState<TrackStatus>(TrackStatus.idle);
YoutubeExplode yt = useMemoized(() => YoutubeExplode());
enum TrackStatus { downloading, idle, done }
var _downloadTrack = useCallback(() async {
if (track == null) return;
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track?.href);
class _DownloadTrackButtonState extends State<DownloadTrackButton> {
late YoutubeExplode yt;
TrackStatus status = TrackStatus.idle;
var audioStream = yt.videos.streamsClient.get(
manifest.audioOnly
.where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(),
);
@override
void initState() {
yt = YoutubeExplode();
super.initState();
}
@override
void dispose() {
yt.close();
super.dispose();
}
_downloadTrack() async {
if (widget.track == null) return;
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(widget.track?.href);
var audioStream = yt.videos.streamsClient
.get(manifest.audioOnly.withHighestBitrate())
.asBroadcastStream();
var statusCb = audioStream.listen(
(event) {
if (status != TrackStatus.downloading) {
setState(() {
status = TrackStatus.downloading;
});
}
},
onDone: () async {
setState(() {
status = TrackStatus.done;
});
await Future.delayed(
const Duration(seconds: 3),
() {
if (status == TrackStatus.done) {
setState(() {
status = TrackStatus.idle;
});
}
},
);
},
);
String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
String fileName =
"${widget.track?.name} - ${artistsToString<Artist>(widget.track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName));
if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true);
IOSink outputFileStream = outputFile.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
await outputFileStream.close().then((value) async {
if (status == TrackStatus.downloading) {
setState(() {
status = TrackStatus.done;
});
var statusCb = audioStream.listen(
(event) {
if (status.value != TrackStatus.downloading) {
status.value = TrackStatus.downloading;
}
},
onDone: () async {
status.value = TrackStatus.done;
await Future.delayed(
const Duration(seconds: 3),
() {
if (status == TrackStatus.done) {
setState(() {
status = TrackStatus.idle;
});
if (status.value == TrackStatus.done) {
status.value = TrackStatus.idle;
}
},
);
}
return statusCb.cancel();
});
}
}
},
);
@override
Widget build(BuildContext context) {
if (status == TrackStatus.downloading) {
String downloadFolder = path.join(
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
String fileName =
"${track?.name} - ${artistsToString<Artist>(track?.artists ?? [])}.mp3";
File outputFile = File(path.join(downloadFolder, fileName));
if (!outputFile.existsSync()) {
outputFile.createSync(recursive: true);
IOSink outputFileStream = outputFile.openWrite();
await audioStream.pipe(outputFileStream);
await outputFileStream.flush();
await outputFileStream.close().then((value) async {
if (status.value == TrackStatus.downloading) {
status.value = TrackStatus.done;
await Future.delayed(
const Duration(seconds: 3),
() {
if (status.value == TrackStatus.done) {
status.value = TrackStatus.idle;
}
},
);
}
return statusCb.cancel();
});
}
}, [track, status, yt]);
useEffect(() {
return () => yt.close();
}, []);
if (status.value == TrackStatus.downloading) {
return const SizedBox(
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
@ -108,13 +88,13 @@ class _DownloadTrackButtonState extends State<DownloadTrackButton> {
height: 20,
width: 20,
);
} else if (status == TrackStatus.done) {
} else if (status.value == TrackStatus.done) {
return const Icon(Icons.download_done_rounded);
}
return IconButton(
icon: const Icon(Icons.download_rounded),
onPressed: widget.track != null &&
!(widget.track!.href ?? "").startsWith("https://api.spotify.com")
onPressed: track != null &&
!(track!.href ?? "").startsWith("https://api.spotify.com")
? _downloadTrack
: null,
);

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
class RecordHotKeyDialog extends StatefulWidget {
class RecordHotKeyDialog extends HookWidget {
final ValueChanged<HotKey> onHotKeyRecorded;
const RecordHotKeyDialog({
@ -9,15 +10,9 @@ class RecordHotKeyDialog extends StatefulWidget {
required this.onHotKeyRecorded,
}) : super(key: key);
@override
_RecordHotKeyDialogState createState() => _RecordHotKeyDialogState();
}
class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
HotKey _hotKey = HotKey(null);
@override
Widget build(BuildContext context) {
var _hotKey = useState(HotKey(null));
return AlertDialog(
content: SingleChildScrollView(
child: ListBody(
@ -58,9 +53,7 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
children: [
HotKeyRecorder(
onHotKeyRecorded: (hotKey) {
setState(() {
_hotKey = hotKey;
});
_hotKey.value = hotKey;
},
),
],
@ -78,10 +71,10 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
),
TextButton(
child: const Text('OK'),
onPressed: !_hotKey.isSetted
onPressed: !_hotKey.value.isSetted
? null
: () {
widget.onHotKeyRecorded(_hotKey);
onHotKeyRecorded(_hotKey.value);
Navigator.of(context).pop();
},
),

View File

@ -1,15 +1,17 @@
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Home.dart';
import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/ThemeProvider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll();
runApp(const ProviderScope(child: MyApp()));
runApp(ProviderScope(child: MyApp()));
doWhenWindowReady(() {
appWindow.minSize = const Size(900, 700);
appWindow.size = const Size(900, 700);
@ -19,56 +21,28 @@ void main() async {
});
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
static _MyAppState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppState>();
class MyApp extends HookConsumerWidget {
@override
State<MyApp> createState() => _MyAppState();
}
Widget build(BuildContext context, ref) {
var themeMode = ref.watch(themeProvider);
useEffect(() {
SharedPreferences.getInstance().then((localStorage) {
String? themeMode = localStorage.getString(LocalStorageKeys.themeMode);
var themeNotifier = ref.read(themeProvider.notifier);
class _MyAppState extends State<MyApp> {
ThemeMode _themeMode = ThemeMode.system;
@override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async {
SharedPreferences localStorage = await SharedPreferences.getInstance();
String? themeMode = localStorage.getString(LocalStorageKeys.themeMode);
setState(() {
switch (themeMode) {
case "light":
_themeMode = ThemeMode.light;
themeNotifier.state = ThemeMode.light;
break;
case "dark":
_themeMode = ThemeMode.dark;
themeNotifier.state = ThemeMode.dark;
break;
default:
_themeMode = ThemeMode.system;
themeNotifier.state = ThemeMode.system;
}
});
});
super.initState();
}
}, []);
void setThemeMode(ThemeMode themeMode) {
SharedPreferences.getInstance().then((localStorage) {
localStorage.setString(
LocalStorageKeys.themeMode, themeMode.toString().split(".").last);
setState(() {
_themeMode = themeMode;
});
});
}
ThemeMode getThemeMode() {
return _themeMode;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Spotube',
@ -156,7 +130,7 @@ class _MyAppState extends State<MyApp> {
),
canvasColor: Colors.blueGrey[900],
),
themeMode: _themeMode,
themeMode: themeMode,
home: const Home(),
);
}

View File

@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
var themeProvider = StateProvider<ThemeMode>((ref) {
return ThemeMode.system;
});

View File

@ -188,6 +188,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
flutter_hooks:
dependency: "direct main"
description:
name: flutter_hooks
url: "https://pub.dartlang.org"
source: hosted
version: "0.18.2+1"
flutter_lints:
dependency: "direct dev"
description:
@ -219,6 +226,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.3"
hooks_riverpod:
dependency: "direct main"
description:
name: hooks_riverpod
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
hotkey_manager:
dependency: "direct main"
description:

View File

@ -50,6 +50,8 @@ dependencies:
path_provider: ^2.0.8
collection: ^1.15.0
flutter_riverpod: ^1.0.3
flutter_hooks: ^0.18.2+1
hooks_riverpod: ^1.0.3
dev_dependencies:
flutter_test: