diff --git a/.env.example b/.env.example index 920fe826b..67d1be8e3 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,7 @@ SPOTIFY_SECRETS= # 0 or 1 # 0 = disable # 1 = enable -ENABLE_UPDATE_CHECK= \ No newline at end of file +ENABLE_UPDATE_CHECK= + +LASTFM_API_KEY= +LASTFM_API_SECRET= diff --git a/.vscode/settings.json b/.vscode/settings.json index 32bb8520f..0e6a4294a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "instrumentalness", "Mpris", "riverpod", + "Scrobblenaut", "speechiness", "Spotube", "winget" diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index 2dd9650ea..ac39cf68a 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -36,6 +36,8 @@ class Assets { static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); static const AssetGenImage placeholder = AssetGenImage('assets/placeholder.png'); + static const AssetGenImage spotubeHeroBanner = + AssetGenImage('assets/spotube-hero-banner.png'); static const AssetGenImage spotubeLogoForeground = AssetGenImage('assets/spotube-logo-foreground.jpg'); static const String spotubeLogoIco = 'assets/spotube-logo.ico'; @@ -53,6 +55,12 @@ class Assets { AssetGenImage('assets/spotube-nightly-logo_android12.png'); static const AssetGenImage spotubeScreenshot = AssetGenImage('assets/spotube-screenshot.png'); + static const AssetGenImage spotubeTallCapsule = + AssetGenImage('assets/spotube-tall-capsule.png'); + static const AssetGenImage spotubeWideCapsuleLarge = + AssetGenImage('assets/spotube-wide-capsule-large.png'); + static const AssetGenImage spotubeWideCapsuleSmall = + AssetGenImage('assets/spotube-wide-capsule-small.png'); static const AssetGenImage spotubeBanner = AssetGenImage('assets/spotube_banner.png'); static const AssetGenImage success = AssetGenImage('assets/success.png'); @@ -67,6 +75,7 @@ class Assets { branding, emptyBox, placeholder, + spotubeHeroBanner, spotubeLogoForeground, spotubeLogoIco, spotubeLogoPng, @@ -77,6 +86,9 @@ class Assets { spotubeNightlyLogoSvg, spotubeNightlyLogoAndroid12, spotubeScreenshot, + spotubeTallCapsule, + spotubeWideCapsuleLarge, + spotubeWideCapsuleSmall, spotubeBanner, success, userPlaceholder diff --git a/lib/collections/env.dart b/lib/collections/env.dart index a6e1efe36..1b9de3de7 100644 --- a/lib/collections/env.dart +++ b/lib/collections/env.dart @@ -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 { diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index faa63da8c..0f83cf3ec 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -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'; @@ -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, diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index c586375f1..9a9c9a92b 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -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; @@ -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; } diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index 4a23cc488..81ccffdb2 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -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'; @@ -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 @@ -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) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c37f93b10..a3c8d9769 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart new file mode 100644 index 000000000..bea43b55d --- /dev/null +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -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(), []); + 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), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart new file mode 100644 index 000000000..837408667 --- /dev/null +++ b/lib/pages/settings/sections/accounts.dart @@ -0,0 +1,128 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.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/settings/section_card_with_heading.dart'; +import 'package:spotube/extensions/constrains.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/provider/scrobbler_provider.dart'; + +class SettingsAccountSection extends HookConsumerWidget { + const SettingsAccountSection({Key? key}) : super(key: key); + + @override + Widget build(context, ref) { + final theme = Theme.of(context); + final auth = ref.watch(AuthenticationNotifier.provider); + final scrobbler = ref.watch(scrobblerProvider); + final router = GoRouter.of(context); + + final logoutBtnStyle = FilledButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ); + + return SectionCardWithHeading( + heading: context.l10n.account, + children: [ + if (auth == null) + LayoutBuilder(builder: (context, constrains) { + return ListTile( + leading: Icon( + SpotubeIcons.spotify, + color: theme.colorScheme.primary, + ), + title: Align( + alignment: Alignment.centerLeft, + child: AutoSizeText( + context.l10n.login_with_spotify, + maxLines: 1, + style: TextStyle( + color: theme.colorScheme.primary, + ), + ), + ), + onTap: constrains.mdAndUp + ? null + : () { + router.push("/login"); + }, + trailing: constrains.smAndDown + ? null + : FilledButton( + onPressed: () { + router.push("/login"); + }, + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25.0), + ), + ), + ), + child: Text( + context.l10n.connect_with_spotify.toUpperCase(), + ), + ), + ); + }) + else + Builder(builder: (context) { + return ListTile( + leading: const Icon(SpotubeIcons.spotify), + title: SizedBox( + height: 50, + width: 180, + child: Align( + alignment: Alignment.centerLeft, + child: AutoSizeText( + context.l10n.logout_of_this_account, + maxLines: 1, + ), + ), + ), + trailing: FilledButton( + style: logoutBtnStyle, + onPressed: () async { + ref.read(AuthenticationNotifier.provider.notifier).logout(); + GoRouter.of(context).pop(); + }, + child: Text(context.l10n.logout), + ), + ); + }), + if (scrobbler == null) + ListTile( + leading: const Icon(SpotubeIcons.lastFm), + title: Text(context.l10n.login_with_lastfm), + subtitle: Text(context.l10n.scrobble_to_lastfm), + trailing: FilledButton.icon( + icon: const Icon(SpotubeIcons.lastFm), + label: Text(context.l10n.connect), + onPressed: () { + router.push("/lastfm-login"); + }, + style: FilledButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 186, 0, 0), + foregroundColor: Colors.white, + ), + ), + ) + else + ListTile( + leading: const Icon(SpotubeIcons.lastFm), + title: Text(context.l10n.disconnect_lastfm), + trailing: FilledButton( + onPressed: () { + ref.read(scrobblerProvider.notifier).logout(); + }, + style: logoutBtnStyle, + child: Text(context.l10n.disconnect), + ), + ), + ], + ); + } +} diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index f537a3a9c..901d32ff3 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -23,7 +23,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/provider/authentication_provider.dart'; +import 'package:spotube/pages/settings/sections/accounts.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/piped_instances_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -34,7 +34,6 @@ class SettingsPage extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final preferences = ref.watch(userPreferencesProvider); - final auth = ref.watch(AuthenticationNotifier.provider); final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); @@ -73,87 +72,7 @@ class SettingsPage extends HookConsumerWidget { constraints: const BoxConstraints(maxWidth: 1366), child: ListView( children: [ - SectionCardWithHeading( - heading: context.l10n.account, - children: [ - if (auth == null) - LayoutBuilder(builder: (context, constrains) { - return ListTile( - leading: Icon( - SpotubeIcons.login, - color: theme.colorScheme.primary, - ), - title: Align( - alignment: Alignment.centerLeft, - child: AutoSizeText( - context.l10n.login_with_spotify, - maxLines: 1, - style: TextStyle( - color: theme.colorScheme.primary, - ), - ), - ), - onTap: constrains.mdAndUp - ? null - : () { - GoRouter.of(context).push("/login"); - }, - trailing: constrains.smAndDown - ? null - : FilledButton( - onPressed: () { - GoRouter.of(context).push("/login"); - }, - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(25.0), - ), - ), - ), - child: Text( - context.l10n.connect_with_spotify - .toUpperCase(), - ), - ), - ); - }) - else - Builder(builder: (context) { - return ListTile( - leading: const Icon(SpotubeIcons.logout), - title: SizedBox( - height: 50, - width: 180, - child: Align( - alignment: Alignment.centerLeft, - child: AutoSizeText( - context.l10n.logout_of_this_account, - maxLines: 1, - ), - ), - ), - trailing: FilledButton( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red), - foregroundColor: - MaterialStateProperty.all(Colors.white), - ), - onPressed: () async { - ref - .read(AuthenticationNotifier - .provider.notifier) - .logout(); - GoRouter.of(context).pop(); - }, - child: Text(context.l10n.logout), - ), - ); - }), - ], - ), + const SettingsAccountSection(), SectionCardWithHeading( heading: context.l10n.language_region, children: [ diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index be01978ed..ed9552dbc 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -19,6 +19,7 @@ import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; +import 'package:spotube/provider/scrobbler_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -52,6 +53,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final Ref ref; late final AudioServices notificationService; + ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier); UserPreferences get preferences => ref.read(userPreferencesProvider); YoutubeEndpoints get youtube => ref.read(youtubeProvider); ProxyPlaylist get playlist => state; @@ -196,12 +198,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } - final (source: _, :segments) = currentSegments.value!; - // skipping in first 2 second breaks stream - if (segments.isEmpty || position < const Duration(seconds: 3)) return; + if (currentSegments.value == null || + currentSegments.value!.segments.isEmpty || + position < const Duration(seconds: 3)) return; - for (final segment in segments) { + for (final segment in currentSegments.value!.segments) { if (position.inSeconds >= segment.start && position.inSeconds < segment.end) { await audioPlayer.seek(Duration(seconds: segment.end)); @@ -607,12 +609,30 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier @override set state(state) { + final hasActiveTrackChanged = super.state.activeTrack is SpotubeTrack + ? state.activeTrack?.id != super.state.activeTrack?.id + : super.state.activeTrack is LocalTrack && + state.activeTrack is LocalTrack + ? (super.state.activeTrack as LocalTrack).path != + (state.activeTrack as LocalTrack).path + : super.state.activeTrack?.id != state.activeTrack?.id; + + final oldTrack = super.state.activeTrack; + super.state = state; if (state.tracks.isEmpty && ref.read(paletteProvider) != null) { ref.read(paletteProvider.notifier).state = null; } else { updatePalette(); } + audioPlayer.position.then((position) { + final isMoreThan30secs = position != null && + (position == Duration.zero || position.inSeconds > 30); + + if (hasActiveTrackChanged && oldTrack != null && isMoreThan30secs) { + scrobbler.scrobble(oldTrack); + } + }); } @override diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart new file mode 100644 index 000000000..a41f722f0 --- /dev/null +++ b/lib/provider/scrobbler_provider.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:catcher/catcher.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/persisted_state_notifier.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; + +class ScrobblerState { + final String username; + final String passwordHash; + + final Scrobblenaut scrobblenaut; + + ScrobblerState({ + required this.username, + required this.passwordHash, + required this.scrobblenaut, + }); + + Map toJson() { + return { + 'username': username, + 'passwordHash': passwordHash, + }; + } +} + +class ScrobblerNotifier extends PersistedStateNotifier { + final Scrobblenaut? scrobblenaut; + + /// Directly scrobbling in set state of [ProxyPlaylistNotifier] + /// brings extra latency in playback + final StreamController _scrobbleController = + StreamController.broadcast(); + + ScrobblerNotifier() + : scrobblenaut = null, + super(null, "scrobbler", encrypted: true) { + _scrobbleController.stream.listen((track) async { + try { + await state?.scrobblenaut.track.scrobble( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + album: track.album!.name!, + chosenByUser: true, + duration: track.duration, + timestamp: DateTime.now().toUtc(), + trackNumber: track.trackNumber, + ); + } catch (e, stackTrace) { + Catcher.reportCheckedError(e, stackTrace); + } + }); + } + + Future login( + String username, + String password, + ) async { + final lastFm = await LastFM.authenticate( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: username, + password: password, + ); + if (!lastFm.isAuth) throw Exception("Invalid credentials"); + state = ScrobblerState( + username: username, + passwordHash: lastFm.passwordHash!, + scrobblenaut: Scrobblenaut(lastFM: lastFm), + ); + } + + Future logout() async { + state = null; + } + + void scrobble(Track track) { + _scrobbleController.add(track); + } + + Future love(Track track) async { + await state?.scrobblenaut.track.love( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + ); + } + + Future unlove(Track track) async { + await state?.scrobblenaut.track.unLove( + artist: TypeConversionUtils.artists_X_String(track.artists!), + track: track.name!, + ); + } + + @override + FutureOr fromJson(Map json) async { + if (json.isEmpty) { + return null; + } + + return ScrobblerState( + username: json['username'], + passwordHash: json['passwordHash'], + scrobblenaut: Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: json["username"], + passwordHash: json["passwordHash"], + ), + ), + ); + } + + @override + Map toJson() { + return state?.toJson() ?? {}; + } +} + +final scrobblerProvider = + StateNotifierProvider( + (ref) => ScrobblerNotifier(), +); diff --git a/pubspec.lock b/pubspec.lock index f45c9bde2..721350bb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -438,10 +438,10 @@ packages: dependency: "direct main" description: name: dio - sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197 + sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7" url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.3.3" disable_battery_optimization: dependency: "direct main" description: @@ -1702,6 +1702,15 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + scrobblenaut: + dependency: "direct main" + description: + path: "." + ref: dart-3-support + resolved-ref: d90cb75d71737f3cfa2de4469d48080c0f3eedc2 + url: "https://github.com/KRTirtho/scrobblenaut.git" + source: git + version: "3.0.0" scroll_to_index: dependency: "direct main" description: @@ -1806,6 +1815,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.0" + simple_icons: + dependency: "direct main" + description: + name: simple_icons + sha256: "8aa6832dc7a263a3213e40ecbf1328a392308c809d534a3b860693625890483b" + url: "https://pub.dev" + source: hosted + version: "7.10.0" skeleton_text: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f820680a6..9ce2c8368 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,6 +81,10 @@ dependencies: permission_handler: ^10.2.0 piped_client: ^0.1.0 popover: ^0.2.6+3 + scrobblenaut: + git: + url: https://github.com/KRTirtho/scrobblenaut.git + ref: dart-3-support scroll_to_index: ^3.0.1 shared_preferences: ^2.0.11 sidebarx: ^0.15.0 @@ -102,6 +106,7 @@ dependencies: ref: a738913c8ce2c9f47515382d40827e794a334274 path: plugins/window_size youtube_explode_dart: ^2.0.1 + simple_icons: ^7.10.0 dev_dependencies: build_runner: ^2.3.2 diff --git a/untranslated_messages.json b/untranslated_messages.json index 8e0df973d..20e54123d 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -6,7 +6,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "bn": [ @@ -16,7 +25,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "ca": [ @@ -26,7 +44,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "de": [ @@ -36,7 +63,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "es": [ @@ -46,7 +82,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "fr": [ @@ -56,7 +101,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "hi": [ @@ -66,7 +120,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "ja": [ @@ -76,7 +139,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "pl": [ @@ -86,7 +158,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "pt": [ @@ -96,7 +177,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "ru": [ @@ -106,7 +196,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "uk": [ @@ -116,7 +215,16 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ], "zh": [ @@ -126,6 +234,15 @@ "add_cover", "restore_defaults", "download_music_codec", - "streaming_music_codec" + "streaming_music_codec", + "login_with_lastfm", + "connect", + "disconnect_lastfm", + "disconnect", + "username", + "password", + "login", + "login_with_your_lastfm", + "scrobble_to_lastfm" ] }