Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: LastFM scrobbling support #761

Merged
merged 3 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ SPOTIFY_SECRETS=
# 0 or 1
# 0 = disable
# 1 = enable
ENABLE_UPDATE_CHECK=
ENABLE_UPDATE_CHECK=

LASTFM_API_KEY=
LASTFM_API_SECRET=
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"instrumentalness",
"Mpris",
"riverpod",
"Scrobblenaut",
"speechiness",
"Spotube",
"winget"
Expand Down
12 changes: 12 additions & 0 deletions lib/collections/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/collections/env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ abstract class Env {
@EnviedField(varName: 'SPOTIFY_SECRETS')
static final String rawSpotifySecrets = _Env.rawSpotifySecrets;

@EnviedField(varName: 'LASTFM_API_KEY')
static final String lastFmApiKey = _Env.lastFmApiKey;

@EnviedField(varName: 'LASTFM_API_SECRET')
static final String lastFmApiSecret = _Env.lastFmApiSecret;

static final spotifySecrets = rawSpotifySecrets.split(',').map((e) {
final secrets = e.trim().split(":").map((e) => e.trim());
return {
Expand Down
7 changes: 7 additions & 0 deletions lib/collections/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/search/search.dart';
Expand Down Expand Up @@ -146,6 +147,12 @@ final router = GoRouter(
child: LoginTutorial(),
),
),
GoRoute(
path: "/lastfm-login",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) =>
const SpotubePage(child: LastFMLoginPage()),
),
GoRoute(
path: "/player",
parentNavigatorKey: rootNavigatorKey,
Expand Down
5 changes: 5 additions & 0 deletions lib/collections/spotube_icons.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:simple_icons/simple_icons.dart';

abstract class SpotubeIcons {
static const home = FluentIcons.home_12_regular;
Expand Down Expand Up @@ -100,4 +101,8 @@ abstract class SpotubeIcons {
static const amoled = FeatherIcons.sunset;
static const file = FeatherIcons.file;
static const stream = Icons.stream_rounded;
static const lastFm = SimpleIcons.lastdotfm;
static const spotify = SimpleIcons.spotify;
static const eye = FeatherIcons.eye;
static const noEye = FeatherIcons.eyeOff;
}
14 changes: 9 additions & 5 deletions lib/components/shared/heart_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart';

Expand Down Expand Up @@ -75,12 +76,12 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {

final mounted = useIsMounted();

final scrobblerNotifier = ref.read(scrobblerProvider.notifier);

final toggleTrackLike = useMutations.track.toggleFavorite(
ref,
track.id!,
onMutate: (isLiked) {
print("Toggle Like onMutate: $isLiked");

if (isLiked) {
savedTracks.setData(
savedTracks.data
Expand All @@ -98,12 +99,15 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
}
return isLiked;
},
onData: (data, recoveryData) async {
print("Toggle Like onData: $data");
onData: (isLiked, recoveryData) async {
await savedTracks.refresh();
if (isLiked) {
await scrobblerNotifier.love(track);
} else {
await scrobblerNotifier.unlove(track);
}
},
onError: (payload, isLiked) {
print("Toggle Like onError: $payload");
if (!mounted()) return;

if (isLiked != true) {
Expand Down
11 changes: 10 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,14 @@
"add_cover": "Add cover",
"restore_defaults": "Restore defaults",
"download_music_codec": "Download music codec",
"streaming_music_codec": "Streaming music codec"
"streaming_music_codec": "Streaming music codec",
"login_with_lastfm": "Login with Last.fm",
"connect": "Connect",
"disconnect_lastfm": "Disconnect Last.fm",
"disconnect": "Disconnect",
"username": "Username",
"password": "Password",
"login": "Login",
"login_with_your_lastfm": "Login with your Last.fm account",
"scrobble_to_lastfm": "Scrobble to Last.fm"
}
127 changes: 127 additions & 0 deletions lib/pages/lastfm_login/lastfm_login.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:form_validator/form_validator.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/scrobbler_provider.dart';

class LastFMLoginPage extends HookConsumerWidget {
const LastFMLoginPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final router = GoRouter.of(context);
final scrobblerNotifier = ref.read(scrobblerProvider.notifier);

final formKey = useMemoized(() => GlobalKey<FormState>(), []);
final username = useTextEditingController();
final password = useTextEditingController();
final passwordVisible = useState(false);

final isLoading = useState(false);

return Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(16.0).copyWith(top: 8),
child: Form(
key: formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: const Color.fromARGB(255, 186, 0, 0),
),
padding: const EdgeInsets.all(12),
child: const Icon(
SpotubeIcons.lastFm,
color: Colors.white,
size: 60,
),
),
Text(
"last.fm",
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(context.l10n.login_with_your_lastfm),
const SizedBox(height: 10),
TextFormField(
controller: username,
validator: ValidationBuilder().required().build(),
decoration: InputDecoration(
labelText: context.l10n.username,
),
),
const SizedBox(height: 10),
TextFormField(
controller: password,
validator: ValidationBuilder().required().build(),
obscureText: !passwordVisible.value,
decoration: InputDecoration(
labelText: context.l10n.password,
suffixIcon: IconButton(
icon: Icon(
passwordVisible.value
? SpotubeIcons.eye
: SpotubeIcons.noEye,
),
onPressed: () =>
passwordVisible.value = !passwordVisible.value,
),
),
),
const SizedBox(height: 10),
FilledButton(
onPressed: isLoading.value
? null
: () async {
try {
isLoading.value = true;
if (formKey.currentState?.validate() != true) {
return;
}
await scrobblerNotifier.login(
username.text,
password.text,
);
router.pop();
} catch (e) {
if (context.mounted) {
showPromptDialog(
context: context,
title: context.l10n
.error("Authentication failed"),
message: e.toString(),
cancelText: null,
);
}
} finally {
isLoading.value = false;
}
},
child: Text(context.l10n.login),
),
],
),
),
),
),
),
),
);
}
}
Loading