diff --git a/.github/workflows/spotube-publish-binary.yml b/.github/workflows/spotube-publish-binary.yml index 812849ac..a5ee3449 100644 --- a/.github/workflows/spotube-publish-binary.yml +++ b/.github/workflows/spotube-publish-binary.yml @@ -4,7 +4,7 @@ on: inputs: version: description: Version to publish (x.x.x) - default: 3.8.0 + default: 3.8.1 required: true dry_run: description: Dry run @@ -76,12 +76,12 @@ jobs: commit_message: Updated to v${{ inputs.version }} winget: - runs-on: windows-latest + runs-on: ubuntu-latest if: contains(inputs.jobs, 'winget') steps: - name: Release winget package if: ${{ !inputs.dry_run }} - uses: vedantmgoyal2009/winget-releaser@v2 + uses: vedantmgoyal9/winget-releaser@main with: version: ${{ inputs.version }} release-tag: v${{ inputs.version }} @@ -134,4 +134,4 @@ jobs: packageName: oss.krtirtho.spotube track: production status: draft - releaseName: ${{ env.TAG_NAME }} \ No newline at end of file + releaseName: ${{ env.TAG_NAME }} diff --git a/.github/workflows/spotube-release-binary.yml b/.github/workflows/spotube-release-binary.yml index b103ea2e..19fbac82 100644 --- a/.github/workflows/spotube-release-binary.yml +++ b/.github/workflows/spotube-release-binary.yml @@ -101,8 +101,15 @@ jobs: - name: Unessary hosted tools if: ${{matrix.platform == 'linux_arm'}} - run: | - sudo rm -rf /usr/share/dotnet + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + swap-storage: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true - name: Build ${{matrix.platform}} binaries run: dart cli/cli.dart build ${{matrix.platform}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e434574..20b48c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [3.8.1](https://github.com/krtirtho/spotube/compare/v3.8.0...v3.8.1) (2024-09-15) + +## Changes + +### Bug Fixes + +- **translations**: correct some basque incorrect translations (#1815) +- **lyrics**: LRCLIB lyrics should be usable without logging in #1803 +- playlist displaying descriptions unescaped html #1784 +- **android**: pressing back while the player is open doesn't take to previous page +- handle dublicated items in playback queue correctly #1852 +- **desktop**: scrollbar overlapping with more options of tracks and playlists +- **discord**: stop discord rpc from try update presence when not connected +- **stats**: minutes page shows plays and streams page shows minutes which should be the opposite #1880 +- **android**: clears queue upon swiping away notification +- **player**: shuffle button state resets after closing page #1657 +- getting started page login page exception #1800 +- **mobile**: queue doesn't persist +- local tracks takes time to load +- start radio not working #1629 + +### Features + +- **desktop**: show error dialog if webview is not found on login #1871 +- manually detect and define touch behavior #1763 + + ## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06) ### Features diff --git a/assets/spotube-logo.bmp b/assets/spotube-logo.bmp new file mode 100644 index 00000000..c3503e85 Binary files /dev/null and b/assets/spotube-logo.bmp differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7e5f24b5..a59f65eb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -58,8 +58,6 @@ PODS: - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - - flutter_keyboard_visibility (0.0.1): - - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): @@ -124,7 +122,6 @@ DEPENDENCIES: - flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`) - flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) @@ -173,8 +170,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_discord_rpc/ios" flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" - flutter_keyboard_visibility: - :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: @@ -220,7 +215,6 @@ SPEC CHECKSUMS: flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 - flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 diff --git a/lib/components/framework/app_pop_scope.dart b/lib/components/framework/app_pop_scope.dart new file mode 100644 index 00000000..b8e35767 --- /dev/null +++ b/lib/components/framework/app_pop_scope.dart @@ -0,0 +1,104 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +/// A temporary workaround for [WillPopScope] and [PopScope] not working in GoRouter +/// https://github.com/flutter/flutter/issues/140869#issuecomment-2247181468 +class AppPopScope extends StatefulWidget { + final Widget child; + + final PopInvokedCallback? onPopInvoked; + + final bool canPop; + + const AppPopScope({ + super.key, + required this.child, + this.canPop = true, + this.onPopInvoked, + }); + + @override + State createState() => _AppPopScopeState(); +} + +class _AppPopScopeState extends State { + final bool _enable = Platform.isAndroid; + ModalRoute? _route; + BackButtonDispatcher? _parentBackBtnDispatcher; + ChildBackButtonDispatcher? _backBtnDispatcher; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _route = ModalRoute.of(context); + _updateBackButtonDispatcher(); + } + + @override + void activate() { + super.activate(); + _updateBackButtonDispatcher(); + } + + @override + void deactivate() { + super.deactivate(); + _disposeBackBtnDispatcher(); + } + + @override + void dispose() { + _disposeBackBtnDispatcher(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: widget.canPop, + onPopInvoked: widget.onPopInvoked, + child: widget.child, + ); + } + + void _updateBackButtonDispatcher() { + if (!_enable) return; + + var dispatcher = Router.maybeOf(context)?.backButtonDispatcher; + if (dispatcher != _parentBackBtnDispatcher) { + _disposeBackBtnDispatcher(); + _parentBackBtnDispatcher = dispatcher; + if (dispatcher is BackButtonDispatcher && + dispatcher is! ChildBackButtonDispatcher) { + dispatcher = dispatcher.createChildBackButtonDispatcher(); + } + _backBtnDispatcher = dispatcher as ChildBackButtonDispatcher; + } + _backBtnDispatcher?.removeCallback(_handleBackButton); + _backBtnDispatcher?.addCallback(_handleBackButton); + _backBtnDispatcher?.takePriority(); + } + + void _disposeBackBtnDispatcher() { + _backBtnDispatcher?.removeCallback(_handleBackButton); + if (_backBtnDispatcher is ChildBackButtonDispatcher) { + final child = _backBtnDispatcher as ChildBackButtonDispatcher; + _parentBackBtnDispatcher?.forget(child); + } + _backBtnDispatcher = null; + _parentBackBtnDispatcher = null; + } + + bool get _onlyRoute => _route != null && _route!.isFirst && _route!.isCurrent; + + Future _handleBackButton() async { + if (_onlyRoute) { + widget.onPopInvoked?.call(widget.canPop); + if (!widget.canPop) { + return true; + } + } + return false; + } +} diff --git a/lib/components/panels/controller.dart b/lib/components/panels/controller.dart index 834e9ce6..4e367701 100644 --- a/lib/components/panels/controller.dart +++ b/lib/components/panels/controller.dart @@ -41,29 +41,33 @@ class PanelController extends ChangeNotifier { bool get isAttached => _panelState != null; /// Closes the sliding panel to its collapsed state (i.e. to the minHeight) - Future close() { + Future close() async { assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._close(); + await _panelState!._close(); + notifyListeners(); } /// Opens the sliding panel fully /// (i.e. to the maxHeight) - Future open() { + Future open() async { assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._open(); + await _panelState!._open(); + notifyListeners(); } /// Hides the sliding panel (i.e. is invisible) - Future hide() { + Future hide() async { assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._hide(); + await _panelState!._hide(); + notifyListeners(); } /// Shows the sliding panel in its collapsed state /// (i.e. "un-hide" the sliding panel) - Future show() { + Future show() async { assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); - return _panelState!._show(); + await _panelState!._show(); + notifyListeners(); } /// Animates the panel position to the value. diff --git a/lib/components/playbutton_card.dart b/lib/components/playbutton_card.dart index d540d31e..ae9050d8 100644 --- a/lib/components/playbutton_card.dart +++ b/lib/components/playbutton_card.dart @@ -58,7 +58,7 @@ class PlaybuttonCard extends HookWidget { others: 15, ); - var unescapeHtml = description?.unescapeHtml(); + final unescapeHtml = description?.unescapeHtml().cleanHtml(); return Container( constraints: BoxConstraints(maxWidth: size), margin: margin, diff --git a/lib/components/track_tile/track_options.dart b/lib/components/track_tile/track_options.dart index 84b0f41f..d2cb92cf 100644 --- a/lib/components/track_tile/track_options.dart +++ b/lib/components/track_tile/track_options.dart @@ -105,7 +105,9 @@ class TrackOptions extends HookConsumerWidget { final pages = await spotify.search.get(query, types: [SearchType.playlist]).first(); - final radios = pages.map((e) => e.items).toList().cast(); + final radios = pages + .expand((e) => e.items?.cast().toList() ?? []) + .toList(); final artists = track.artists!.map((e) => e.name); diff --git a/lib/components/track_tile/track_tile.dart b/lib/components/track_tile/track_tile.dart index 12ce063f..8ab889f8 100644 --- a/lib/components/track_tile/track_tile.dart +++ b/lib/components/track_tile/track_tile.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:spotify/spotify.dart'; @@ -21,6 +22,7 @@ import 'package:spotube/pages/track/track.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/blacklist_provider.dart'; +import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class TrackTile extends HookConsumerWidget { @@ -276,6 +278,7 @@ class TrackTile extends HookConsumerWidget { userPlaylist: userPlaylist, showMenuCbRef: showOptionCbRef, ), + if (kIsDesktop) const Gap(10), ], ), ), diff --git a/lib/components/tracks_view/sections/body/track_view_body.dart b/lib/components/tracks_view/sections/body/track_view_body.dart index df841b8d..0f161b0c 100644 --- a/lib/components/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/tracks_view/sections/body/track_view_body.dart @@ -15,6 +15,7 @@ import 'package:spotube/components/tracks_view/sections/body/track_view_body_hea import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart'; import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_provider.dart'; +import 'package:spotube/extensions/list.dart'; import 'package:spotube/models/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/history/history.dart'; @@ -65,6 +66,56 @@ class TrackViewBodySection extends HookConsumerWidget { final isActive = playlist.collections.contains(props.collectionId); + final onTapTrackTile = useCallback((Track track, int index) async { + if (trackViewState.isSelecting) { + trackViewState.toggleTrackSelection(track.id!); + return; + } + + final isRemoteDevice = await showSelectDeviceDialog(context, ref); + + if (isRemoteDevice) { + final remotePlayback = ref.read(connectProvider.notifier); + final remoteQueue = ref.read(queueProvider); + if (remoteQueue.collections.contains(props.collectionId) || + remoteQueue.tracks.any((s) => s.id == track.id)) { + await playlistNotifier.jumpToTrack(track); + } else { + final tracks = await props.pagination.onFetchAll(); + await remotePlayback.load( + props.collection is AlbumSimple + ? WebSocketLoadEventData.album( + tracks: tracks, + collection: props.collection as AlbumSimple, + initialIndex: index, + ) + : WebSocketLoadEventData.playlist( + tracks: tracks, + collection: props.collection as PlaylistSimple, + initialIndex: index, + ), + ); + } + } else { + if (isActive || playlist.tracks.containsBy(track, (a) => a.id)) { + await playlistNotifier.jumpToTrack(track); + } else { + final tracks = await props.pagination.onFetchAll(); + await playlistNotifier.load( + tracks, + initialIndex: index, + autoPlay: true, + ); + playlistNotifier.addCollection(props.collectionId); + if (props.collection is AlbumSimple) { + historyNotifier.addAlbums([props.collection as AlbumSimple]); + } else { + historyNotifier.addPlaylists([props.collection as PlaylistSimple]); + } + } + } + }, [isActive, playlist, props, playlistNotifier, historyNotifier]); + return SliverMainAxisGroup( slivers: [ SliverToBoxAdapter( @@ -130,58 +181,7 @@ class TrackViewBodySection extends HookConsumerWidget { trackViewState.selectTrack(track.id!); HapticFeedback.selectionClick(); }, - onTap: () async { - if (trackViewState.isSelecting) { - trackViewState.toggleTrackSelection(track.id!); - return; - } - - final isRemoteDevice = - await showSelectDeviceDialog(context, ref); - - if (isRemoteDevice) { - final remotePlayback = ref.read(connectProvider.notifier); - final remoteQueue = ref.read(queueProvider); - if (remoteQueue.collections.contains(props.collectionId) || - remoteQueue.tracks.any((s) => s.id == track.id)) { - await playlistNotifier.jumpToTrack(track); - } else { - final tracks = await props.pagination.onFetchAll(); - await remotePlayback.load( - props.collection is AlbumSimple - ? WebSocketLoadEventData.album( - tracks: tracks, - collection: props.collection as AlbumSimple, - initialIndex: index, - ) - : WebSocketLoadEventData.playlist( - tracks: tracks, - collection: props.collection as PlaylistSimple, - initialIndex: index, - ), - ); - } - } else { - if (isActive || playlist.tracks.contains(track)) { - await playlistNotifier.jumpToTrack(track); - } else { - final tracks = await props.pagination.onFetchAll(); - await playlistNotifier.load( - tracks, - initialIndex: index, - autoPlay: true, - ); - playlistNotifier.addCollection(props.collectionId); - if (props.collection is AlbumSimple) { - historyNotifier - .addAlbums([props.collection as AlbumSimple]); - } else { - historyNotifier - .addPlaylists([props.collection as PlaylistSimple]); - } - } - } - }, + onTap: () => onTapTrackTile(track, index), ); }, ), diff --git a/lib/components/tracks_view/sections/body/track_view_body_headers.dart b/lib/components/tracks_view/sections/body/track_view_body_headers.dart index 564c85d0..82cc7706 100644 --- a/lib/components/tracks_view/sections/body/track_view_body_headers.dart +++ b/lib/components/tracks_view/sections/body/track_view_body_headers.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/sort_tracks_dropdown.dart'; @@ -7,6 +8,7 @@ import 'package:spotube/components/tracks_view/track_view_props.dart'; import 'package:spotube/components/tracks_view/track_view_provider.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/utils/platform.dart'; class TrackViewBodyHeaders extends HookConsumerWidget { final ValueNotifier isFiltering; @@ -94,6 +96,7 @@ class TrackViewBodyHeaders extends HookConsumerWidget { }, ), const TrackViewBodyOptions(), + if (kIsDesktop) const Gap(10), ], ); }, diff --git a/lib/components/tracks_view/sections/header/flexible_header.dart b/lib/components/tracks_view/sections/header/flexible_header.dart index 6845cc3e..508d289c 100644 --- a/lib/components/tracks_view/sections/header/flexible_header.dart +++ b/lib/components/tracks_view/sections/header/flexible_header.dart @@ -128,7 +128,9 @@ class TrackViewFlexHeader extends HookConsumerWidget { if (props.description != null && props.description!.isNotEmpty) Text( - props.description!.unescapeHtml(), + props.description! + .unescapeHtml() + .cleanHtml(), style: defaultTextStyle.style.copyWith( color: palette.bodyTextColor, diff --git a/lib/extensions/list.dart b/lib/extensions/list.dart new file mode 100644 index 00000000..ddd36e4d --- /dev/null +++ b/lib/extensions/list.dart @@ -0,0 +1,19 @@ +extension UniqueItemExtension on List { + List unique(bool Function(T a, T b) equals) { + final copy = []; + + for (final item in this) { + if (copy.any((element) => equals(element, item))) continue; + copy.add(item); + } + + return copy; + } + + bool containsBy(T item, dynamic Function(T a) fn) { + for (final el in this) { + if (fn(el) == fn(item)) return true; + } + return false; + } +} diff --git a/lib/extensions/string.dart b/lib/extensions/string.dart index d3706f3f..94123fe3 100644 --- a/lib/extensions/string.dart +++ b/lib/extensions/string.dart @@ -1,12 +1,15 @@ import 'package:html_unescape/html_unescape.dart'; +import 'package:html/parser.dart'; final htmlEscape = HtmlUnescape(); extension UnescapeHtml on String { + String cleanHtml() => parse("

$this

").documentElement!.text; String unescapeHtml() => htmlEscape.convert(this); } extension NullableUnescapeHtml on String? { + String? cleanHtml() => this?.cleanHtml(); String? unescapeHtml() => this?.unescapeHtml(); } diff --git a/lib/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart index 90d062dc..0bb27a11 100644 --- a/lib/hooks/configurators/use_deep_linking.dart +++ b/lib/hooks/configurators/use_deep_linking.dart @@ -7,6 +7,7 @@ import 'package:spotube/collections/routes.dart'; import 'package:spotube/provider/spotify_provider.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/utils/platform.dart'; final appLinks = AppLinks(); @@ -61,30 +62,34 @@ void useDeepLinking(WidgetRef ref) { } final subscription = linkStream.listen((uri) async { - final startSegment = uri.split(":").take(2).join(":"); - final endSegment = uri.split(":").last; + try { + final startSegment = uri.split(":").take(2).join(":"); + final endSegment = uri.split(":").last; - switch (startSegment) { - case "spotify:album": - await router.push( - "/album/$endSegment", - extra: await spotify.albums.get(endSegment), - ); - break; - case "spotify:artist": - await router.push("/artist/$endSegment"); - break; - case "spotify:track": - await router.push("/track/$endSegment"); - break; - case "spotify:playlist": - await router.push( - "/playlist/$endSegment", - extra: await spotify.playlists.get(endSegment), - ); - break; - default: - break; + switch (startSegment) { + case "spotify:album": + await router.push( + "/album/$endSegment", + extra: await spotify.albums.get(endSegment), + ); + break; + case "spotify:artist": + await router.push("/artist/$endSegment"); + break; + case "spotify:track": + await router.push("/track/$endSegment"); + break; + case "spotify:playlist": + await router.push( + "/playlist/$endSegment", + extra: await spotify.playlists.get(endSegment), + ); + break; + default: + break; + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); diff --git a/lib/hooks/configurators/use_has_touch.dart b/lib/hooks/configurators/use_has_touch.dart new file mode 100644 index 00000000..75353f27 --- /dev/null +++ b/lib/hooks/configurators/use_has_touch.dart @@ -0,0 +1,27 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/utils/platform.dart'; + +bool useHasTouch() { + final hasTouch = useState(kIsMobile); + + useEffect(() { + void globalRoute(PointerEvent event) { + if (hasTouch.value) return; + hasTouch.value = event.kind == PointerDeviceKind.touch || + event.kind == PointerDeviceKind.stylus || + event.kind == PointerDeviceKind.invertedStylus; + } + + WidgetsBinding.instance.addPostFrameCallback((_) { + GestureBinding.instance.pointerRouter.addGlobalRoute(globalRoute); + }); + + return () { + GestureBinding.instance.pointerRouter.removeGlobalRoute(globalRoute); + }; + }, []); + + return hasTouch.value; +} diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index a962b41b..141e10f0 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "حصلت على حبك", "summary_playlists": "قوائم التشغيل", "summary_were_on_repeat": "كانت على التكرار", - "total_money": "المجموع {money}" + "total_money": "المجموع {money}", + "webview_not_found": "لم يتم العثور على Webview", + "webview_not_found_description": "لم يتم تثبيت بيئة تشغيل Webview على جهازك.\nإذا كانت مثبتة، تأكد من وجودها في environment PATH\n\nبعد التثبيت، أعد تشغيل التطبيق", + "unsupported_platform": "المنصة غير مدعومة" } \ No newline at end of file diff --git a/lib/l10n/app_bn.arb b/lib/l10n/app_bn.arb index 97872c8c..ae088b45 100644 --- a/lib/l10n/app_bn.arb +++ b/lib/l10n/app_bn.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "আপনার ভালোবাসা পেয়েছে", "summary_playlists": "প্লেলিস্ট", "summary_were_on_repeat": "পুনরাবৃত্তিতে ছিল", - "total_money": "মোট {money}" + "total_money": "মোট {money}", + "webview_not_found": "ওয়েবভিউ পাওয়া যায়নি", + "webview_not_found_description": "আপনার ডিভাইসে কোনো ওয়েবভিউ রানটাইম ইনস্টল করা নেই।\nযদি ইনস্টল থাকে, তা নিশ্চিত করুন যে এটি environment PATH এ রয়েছে\n\nইনস্টল করার পর, অ্যাপটি পুনরায় চালু করুন", + "unsupported_platform": "সমর্থিত প্ল্যাটফর্ম নয়" } \ No newline at end of file diff --git a/lib/l10n/app_ca.arb b/lib/l10n/app_ca.arb index 2cda6e88..58805e62 100644 --- a/lib/l10n/app_ca.arb +++ b/lib/l10n/app_ca.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "ha aconseguit el teu amor", "summary_playlists": "llistes de reproducció", "summary_were_on_repeat": "estaven en repetició", - "total_money": "total {money}" + "total_money": "total {money}", + "webview_not_found": "No s'ha trobat el Webview", + "webview_not_found_description": "No hi ha cap temps d'execució de Webview instal·lat al dispositiu.\nSi està instal·lat, assegureu-vos que estigui en el environment PATH\n\nDesprés d'instal·lar-lo, reinicieu l'aplicació", + "unsupported_platform": "Plataforma no compatible" } \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index b1a22ee2..99ee0962 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Získal vaši lásku", "summary_playlists": "playlisty", "summary_were_on_repeat": "Byly na opakování", - "total_money": "Celkem {money}" + "total_money": "Celkem {money}", + "webview_not_found": "Webview nebyl nalezen", + "webview_not_found_description": "Na vašem zařízení není nainstalováno žádné runtime prostředí Webview.\nPokud je nainstalováno, ujistěte se, že je v environment PATH\n\nPo instalaci restartujte aplikaci", + "unsupported_platform": "Nepodporovaná platforma" } \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 4b9495aa..36da0b3e 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Hat Ihre Liebe gewonnen", "summary_playlists": "Wiedergabelisten", "summary_were_on_repeat": "Wurden wiederholt", - "total_money": "Gesamt {money}" + "total_money": "Gesamt {money}", + "webview_not_found": "Webview nicht gefunden", + "webview_not_found_description": "Es ist keine Webview-Laufzeitumgebung auf Ihrem Gerät installiert.\nFalls installiert, stellen Sie sicher, dass es im environment PATH ist\n\nNach der Installation starten Sie die App neu", + "unsupported_platform": "Nicht unterstützte Plattform" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 06a90d79..c63f8543 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Got your love", "summary_playlists": "playlists", "summary_were_on_repeat": "Were on repeat", - "total_money": "Total {money}" + "total_money": "Total {money}", + "webview_not_found": "Webview not found", + "webview_not_found_description": "No webview runtime is installed in your device.\nIf it's installed make sure it's in the Environment PATH\n\nAfter installing, restart the app", + "unsupported_platform": "Unsupported platform" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 6834d845..d3c8b389 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Obtuvo tu amor", "summary_playlists": "listas de reproducción", "summary_were_on_repeat": "Estaban en repetición", - "total_money": "Total {money}" + "total_money": "Total {money}", + "webview_not_found": "No se encontró el Webview", + "webview_not_found_description": "No hay tiempo de ejecución de Webview instalado en su dispositivo.\nSi está instalado, asegúrese de que esté en el environment PATH\n\nDespués de instalar, reinicie la aplicación", + "unsupported_platform": "Plataforma no soportada" } \ No newline at end of file diff --git a/lib/l10n/app_eu.arb b/lib/l10n/app_eu.arb index 6cc41620..36986804 100644 --- a/lib/l10n/app_eu.arb +++ b/lib/l10n/app_eu.arb @@ -367,7 +367,7 @@ "count_plays": "{count} erreprodukzio", "streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)", "minutes_listened": "Entzundako minutuak", - "streamed_songs": "Stream-eatutako kantak", + "streamed_songs": "Streaming-ez entzundako kantak", "count_streams": "{count} stream", "owned_by_you": "Zure jabetzakoa", "copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua", @@ -376,13 +376,16 @@ "summary_minutes": "minutu", "summary_listened_to_music": "Musika entzuten", "summary_songs": "kanta", - "summary_streamed_overall": "Stream-eatuta oro har", + "summary_streamed_overall": "Streaming abesti oro har", "summary_owed_to_artists": "Hilabete honetan\nartistei zor zaiena", "summary_artists": "artisten", "summary_music_reached_you": "Musika ailegatu zaizu", "summary_full_albums": "album osok", - "summary_got_your_love": "Izan dute zure maitasuna", + "summary_got_your_love": "Jaso dute zure maitasuna", "summary_playlists": "zerrenda", "summary_were_on_repeat": "Dituzu errepikatze moduan", - "total_money": "Guztira {money}" + "total_money": "Guztira {money}", + "webview_not_found": "Ez da Webview aurkitu", + "webview_not_found_description": "Ez dago Webview abiarazte denbora-instalaziorik zure gailuan.\nInstalatuta badago, ziurtatu environment PATH-an dagoela\n\nInstalatu ondoren, berrabiarazi aplikazioa", + "unsupported_platform": "Plataforma ez onartua" } \ No newline at end of file diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 5611e0cc..47242a04 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "عشق شما را به دست آورد", "summary_playlists": "لیست‌های پخش", "summary_were_on_repeat": "در تکرار بودند", - "total_money": "مجموع {money}" + "total_money": "مجموع {money}", + "webview_not_found": "وب‌ویو پیدا نشد", + "webview_not_found_description": "هیچ اجرای وب‌ویو روی دستگاه شما نصب نشده است.\nدر صورت نصب، مطمئن شوید که در environment PATH قرار دارد\n\nپس از نصب، برنامه را مجدداً راه‌اندازی کنید", + "unsupported_platform": "پلتفرم پشتیبانی نمی‌شود" } \ No newline at end of file diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 57f209ab..53b948a6 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Sai rakkautesi", "summary_playlists": "soittolistat", "summary_were_on_repeat": "Olivat toistossa", - "total_money": "Yhteensä {money}" + "total_money": "Yhteensä {money}", + "webview_not_found": "Webview ei löydy", + "webview_not_found_description": "Laitteellasi ei ole asennettua Webview-ajonaikaa.\nJos se on asennettu, varmista, että se on environment PATH:ssa\n\nAsennuksen jälkeen käynnistä sovellus uudelleen", + "unsupported_platform": "Ei tuettu alusta" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4a41dec9..522a2af4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "A obtenu votre amour", "summary_playlists": "playlists", "summary_were_on_repeat": "Était en répétition", - "total_money": "Total {money}" + "total_money": "Total {money}", + "webview_not_found": "Webview non trouvé", + "webview_not_found_description": "Aucun environnement d'exécution Webview installé sur votre appareil.\nSi c'est installé, assurez-vous qu'il soit dans le environment PATH\n\nAprès l'installation, redémarrez l'application", + "unsupported_platform": "Plateforme non prise en charge" } \ No newline at end of file diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index a65e3f75..ce01aebe 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -384,5 +384,8 @@ "count_streams": "{count} स्ट्रिम", "owned_by_you": "तपाईंले स्वामित्व गरेको", "copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", - "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।" + "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।", + "webview_not_found": "वेबव्यू नहीं मिला", + "webview_not_found_description": "आपके डिवाइस पर वेबव्यू रनटाइम इंस्टॉल नहीं है।\nअगर इंस्टॉल है, तो सुनिश्चित करें कि यह environment PATH में है\n\nइंस्टॉल करने के बाद, ऐप को पुनः शुरू करें", + "unsupported_platform": "असमर्थित प्लेटफार्म" } \ No newline at end of file diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 0a417c40..121695f4 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -384,5 +384,8 @@ "summary_got_your_love": "Mendapatkan cinta Anda", "summary_playlists": "daftar putar", "summary_were_on_repeat": "Sedang diulang", - "total_money": "Total {money}" + "total_money": "Total {money}", + "webview_not_found": "Webview tidak ditemukan", + "webview_not_found_description": "Tidak ada runtime Webview yang diinstal di perangkat Anda.\nJika sudah diinstal, pastikan itu ada di environment PATH\n\nSetelah diinstal, restart aplikasi", + "unsupported_platform": "Platform tidak didukung" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 6cbcbb6a..3a2c57c3 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -385,5 +385,8 @@ "summary_got_your_love": "Ha ricevuto il tuo amore", "summary_playlists": "playlist", "summary_were_on_repeat": "Erano in ripetizione", - "total_money": "Totale {money}" + "total_money": "Totale {money}", + "webview_not_found": "Webview non trovato", + "webview_not_found_description": "Nessun runtime Webview installato nel tuo dispositivo.\nSe è installato, assicurati che sia nel environment PATH\n\nDopo l'installazione, riavvia l'app", + "unsupported_platform": "Piattaforma non supportata" } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index a26c8ba0..ed779478 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -384,5 +384,8 @@ "count_streams": "{count} 回のストリーム", "owned_by_you": "あなたが所有", "copied_shareurl_to_clipboard": "{shareUrl} をクリップボードにコピーしました", - "spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。" + "spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。", + "webview_not_found": "Webviewが見つかりません", + "webview_not_found_description": "デバイスにWebviewランタイムがインストールされていません。\nインストールされている場合は、environment PATHにあることを確認してください\n\nインストール後、アプリを再起動してください", + "unsupported_platform": "サポートされていないプラットフォーム" } \ No newline at end of file diff --git a/lib/l10n/app_ka.arb b/lib/l10n/app_ka.arb index 66d7f888..888dbb6f 100644 --- a/lib/l10n/app_ka.arb +++ b/lib/l10n/app_ka.arb @@ -384,5 +384,8 @@ "count_streams": "{count} სტრიმი", "owned_by_you": "შენ მიერ საკუთრებული", "copied_shareurl_to_clipboard": "{shareUrl} აიღო კლიპბორდზე", - "spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე." + "spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე.", + "webview_not_found": "ვებვიუ ვერ მოიძებნა", + "webview_not_found_description": "თქვენს მოწყობილობაზე ვებვიუის შესრულების დრო არ არის დაყენებული.\nთუ დაყენებულია, დარწმუნდით, რომ ის environment PATH-შია\n\nდაყენების შემდეგ, გადატვირთეთ აპი", + "unsupported_platform": "მოუხერხებელი პლატფორმა" } \ No newline at end of file diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 10036ba5..a71b59ae 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -385,5 +385,8 @@ "count_streams": "{count} 스트림", "owned_by_you": "당신이 소유", "copied_shareurl_to_clipboard": "{shareUrl}를 클립보드에 복사했습니다", - "spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다." + "spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다.", + "webview_not_found": "웹뷰를 찾을 수 없음", + "webview_not_found_description": "기기에 웹뷰 런타임이 설치되지 않았습니다.\n설치되어 있으면 environment PATH에 있는지 확인하십시오\n\n설치 후 앱을 다시 시작하세요", + "unsupported_platform": "지원되지 않는 플랫폼" } \ No newline at end of file diff --git a/lib/l10n/app_ne.arb b/lib/l10n/app_ne.arb index ce2a1e4b..9bcfebad 100644 --- a/lib/l10n/app_ne.arb +++ b/lib/l10n/app_ne.arb @@ -384,5 +384,8 @@ "count_streams": "{count} स्ट्रिम", "owned_by_you": "तपाईंले स्वामित्व गरेको", "copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", - "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।" + "spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।", + "webview_not_found": "वेबभ्यू फेला परेन", + "webview_not_found_description": "तपाईंको उपकरणमा कुनै वेबभ्यू रनटाइम स्थापना गरिएको छैन।\nयदि स्थापना गरिएको छ भने, environment PATH मा छ कि छैन भनेर सुनिश्चित गर्नुहोस्\n\nस्थापना पछि, अनुप्रयोग पुनः सुरु गर्नुहोस्", + "unsupported_platform": "असमर्थित प्लेटफार्म" } \ No newline at end of file diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 5e22446d..93ab02a1 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -385,5 +385,8 @@ "count_streams": "{count} streams", "owned_by_you": "Bezit door jou", "copied_shareurl_to_clipboard": "{shareUrl} gekopieerd naar klembord", - "spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren." + "spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren.", + "webview_not_found": "Webview niet gevonden", + "webview_not_found_description": "Er is geen Webview-runtime geïnstalleerd op uw apparaat.\nAls het is geïnstalleerd, zorg ervoor dat het in het environment PATH staat\n\nHerstart de app na installatie", + "unsupported_platform": "Niet ondersteund platform" } \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 06449ad9..c003ef08 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -384,5 +384,8 @@ "count_streams": "{count} strumieni", "owned_by_you": "Własność Twoja", "copied_shareurl_to_clipboard": "{shareUrl} skopiowano do schowka", - "spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify." + "spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify.", + "webview_not_found": "Nie znaleziono Webview", + "webview_not_found_description": "Na twoim urządzeniu nie zainstalowano środowiska uruchomieniowego Webview.\nJeśli jest zainstalowany, upewnij się, że jest w environment PATH\n\nPo instalacji uruchom ponownie aplikację", + "unsupported_platform": "Nieobsługiwana platforma" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7231d15a..02772b1e 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -384,5 +384,8 @@ "count_streams": "{count} streams", "owned_by_you": "De sua propriedade", "copied_shareurl_to_clipboard": "{shareUrl} copiado para a área de transferência", - "spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify." + "spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify.", + "webview_not_found": "Webview não encontrado", + "webview_not_found_description": "Nenhum runtime Webview está instalado no seu dispositivo.\nSe estiver instalado, certifique-se de que está no environment PATH\n\nApós a instalação, reinicie o aplicativo", + "unsupported_platform": "Plataforma não suportada" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7cffb42a..189e644f 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -384,5 +384,8 @@ "count_streams": "{count} стримов", "owned_by_you": "Ваша собственность", "copied_shareurl_to_clipboard": "{shareUrl} скопировано в буфер обмена", - "spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify." + "spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.", + "webview_not_found": "Webview не найден", + "webview_not_found_description": "На вашем устройстве не установлена среда выполнения Webview.\nЕсли он установлен, убедитесь, что он находится в environment PATH\n\nПосле установки перезапустите приложение", + "unsupported_platform": "Платформа не поддерживается" } \ No newline at end of file diff --git a/lib/l10n/app_th.arb b/lib/l10n/app_th.arb index 3cac73f7..27c05a5d 100644 --- a/lib/l10n/app_th.arb +++ b/lib/l10n/app_th.arb @@ -385,5 +385,8 @@ "count_streams": "{count} สตรีม", "owned_by_you": "เป็นเจ้าของโดยคุณ", "copied_shareurl_to_clipboard": "{shareUrl} คัดลอกไปที่คลิปบอร์ดแล้ว", - "spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify." + "spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify.", + "webview_not_found": "ไม่พบ Webview", + "webview_not_found_description": "ไม่พบ runtime ของ Webview บนอุปกรณ์ของคุณ\nหากติดตั้งแล้วตรวจสอบให้แน่ใจว่าอยู่ใน environment PATH\n\nหลังจากติดตั้งแล้ว ให้รีสตาร์ทแอป", + "unsupported_platform": "แพลตฟอร์มไม่รองรับ" } \ No newline at end of file diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index b5a0ec1e..230f14e8 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -384,5 +384,8 @@ "count_streams": "{count} yayın", "owned_by_you": "Sahip olduğunuz", "copied_shareurl_to_clipboard": "{shareUrl} panoya kopyalandı", - "spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir." + "spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir.", + "webview_not_found": "Webview bulunamadı", + "webview_not_found_description": "Cihazınızda herhangi bir Webview çalışma zamanı yüklü değil.\nEğer kuruluysa, ortam YOLUNDA olduğundan emin olun\n\nKurulumdan sonra uygulamayı yeniden başlatın", + "unsupported_platform": "Desteklenmeyen platform" } \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 013a64b7..0c65f756 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -384,5 +384,8 @@ "count_streams": "{count} стримів", "owned_by_you": "Ваша власність", "copied_shareurl_to_clipboard": "{shareUrl} скопійовано в буфер обміну", - "spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify." + "spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify.", + "webview_not_found": "Webview не знайдено", + "webview_not_found_description": "На вашому пристрої не встановлено виконуване середовище Webview.\nЯкщо воно встановлено, переконайтеся, що воно знаходиться в environment PATH\n\nПісля встановлення перезапустіть програму", + "unsupported_platform": "Непідтримувана платформа" } \ No newline at end of file diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 5791793e..75dc1532 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -384,5 +384,8 @@ "count_streams": "{count} lượt phát", "owned_by_you": "Thuộc sở hữu của bạn", "copied_shareurl_to_clipboard": "{shareUrl} đã sao chép vào bảng tạm", - "spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify." + "spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify.", + "webview_not_found": "Không tìm thấy Webview", + "webview_not_found_description": "Không có runtime Webview nào được cài đặt trên thiết bị của bạn.\nNếu đã cài đặt, hãy đảm bảo rằng nó nằm trong environment PATH\n\nSau khi cài đặt, hãy khởi động lại ứng dụng", + "unsupported_platform": "Nền tảng không được hỗ trợ" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 91447213..c9bf35df 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -384,5 +384,8 @@ "count_streams": "{count} 次流媒体", "owned_by_you": "由您拥有", "copied_shareurl_to_clipboard": "{shareUrl} 已复制到剪贴板", - "spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算,用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。" + "spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算,用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。", + "webview_not_found": "未找到 Webview", + "webview_not_found_description": "您的设备中未安装 Webview 运行时。\n如果已安装,请确保它在 environment PATH 中\n\n安装后,重新启动应用程序", + "unsupported_platform": "不支持的平台" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 64710f47..f13991e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/foundation.dart'; @@ -22,6 +23,7 @@ import 'package:spotube/hooks/configurators/use_deep_linking.dart'; import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart'; import 'package:spotube/hooks/configurators/use_fix_window_stretching.dart'; import 'package:spotube/hooks/configurators/use_get_storage_perms.dart'; +import 'package:spotube/hooks/configurators/use_has_touch.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; @@ -92,7 +94,7 @@ Future main(List rawArgs) async { await FlutterDiscordRPC.initialize(Env.discordAppId); } - if(kIsWindows){ + if (kIsWindows) { await SMTCWindows.initialize(); } @@ -142,6 +144,7 @@ class Spotube extends HookConsumerWidget { final paletteColor = ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); + final hasTouchSupport = useHasTouch(); ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {}); @@ -191,8 +194,22 @@ class Spotube extends HookConsumerWidget { debugShowCheckedModeBanner: false, title: 'Spotube', builder: (context, child) { - if (kIsDesktop && !kIsMacOS) return DragToResizeArea(child: child!); - return child!; + child = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: hasTouchSupport + ? { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + } + : null, + ), + child: child!, + ); + + if (kIsDesktop && !kIsMacOS) child = DragToResizeArea(child: child); + + return child; }, themeMode: themeMode, theme: lightTheme, diff --git a/lib/modules/lyrics/use_synced_lyrics.dart b/lib/modules/lyrics/use_synced_lyrics.dart index 7a171473..cf929226 100644 --- a/lib/modules/lyrics/use_synced_lyrics.dart +++ b/lib/modules/lyrics/use_synced_lyrics.dart @@ -1,6 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; int useSyncedLyrics( WidgetRef ref, @@ -13,8 +14,12 @@ int useSyncedLyrics( useEffect(() { return stream.listen((pos) { - if (lyricsMap.containsKey(pos.inSeconds + delay)) { - currentTime.value = pos.inSeconds + delay; + try { + if (lyricsMap.containsKey(pos.inSeconds + delay)) { + currentTime.value = pos.inSeconds + delay; + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }).cancel; }, [lyricsMap, delay]); diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 3202eeda..925afadc 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/framework/app_pop_scope.dart'; import 'package:spotube/modules/player/player_actions.dart'; import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; @@ -100,11 +101,10 @@ class PlayerView extends HookConsumerWidget { final topPadding = MediaQueryData.fromView(View.of(context)).padding.top; - // ignore: deprecated_member_use - return WillPopScope( - onWillPop: () async { + return AppPopScope( + canPop: context.canPop(), + onPopInvoked: (didPop) async { await panelController.close(); - return false; }, child: IconTheme( data: theme.iconTheme.copyWith(color: bodyTextColor), diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index c88f6258..12288a3d 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -170,27 +170,26 @@ class PlayerControls extends HookConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - StreamBuilder( - stream: audioPlayer.shuffledStream, - builder: (context, snapshot) { - final shuffled = snapshot.data ?? false; - return IconButton( - tooltip: shuffled - ? context.l10n.unshuffle_playlist - : context.l10n.shuffle_playlist, - icon: const Icon(SpotubeIcons.shuffle), - style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: isFetchingActiveTrack - ? null - : () { - if (shuffled) { - audioPlayer.setShuffle(false); - } else { - audioPlayer.setShuffle(true); - } - }, - ); - }), + Consumer(builder: (context, ref, _) { + final shuffled = ref + .watch(audioPlayerProvider.select((s) => s.shuffled)); + return IconButton( + tooltip: shuffled + ? context.l10n.unshuffle_playlist + : context.l10n.shuffle_playlist, + icon: const Icon(SpotubeIcons.shuffle), + style: shuffled ? activeButtonStyle : buttonStyle, + onPressed: isFetchingActiveTrack + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, + ); + }), IconButton( tooltip: context.l10n.previous_track, icon: const Icon(SpotubeIcons.skipBack), diff --git a/lib/modules/root/bottom_player.dart b/lib/modules/root/bottom_player.dart index 7f37c472..a2f45449 100644 --- a/lib/modules/root/bottom_player.dart +++ b/lib/modules/root/bottom_player.dart @@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:flutter/material.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; @@ -30,7 +29,6 @@ class BottomPlayer extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final auth = ref.watch(authenticationProvider); final playlist = ref.watch(audioPlayerProvider); final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); @@ -89,35 +87,34 @@ class BottomPlayer extends HookConsumerWidget { children: [ PlayerActions( extraActions: [ - if (auth.asData?.value != null) - IconButton( - tooltip: context.l10n.mini_player, - icon: const Icon(SpotubeIcons.miniPlayer), - onPressed: () async { - if (!kIsDesktop) return; + IconButton( + tooltip: context.l10n.mini_player, + icon: const Icon(SpotubeIcons.miniPlayer), + onPressed: () async { + if (!kIsDesktop) return; - final prevSize = await windowManager.getSize(); - await windowManager.setMinimumSize( - const Size(300, 300), - ); - await windowManager.setAlwaysOnTop(true); - if (!kIsLinux) { - await windowManager.setHasShadow(false); - } - await windowManager - .setAlignment(Alignment.topRight); - await windowManager.setSize(const Size(400, 500)); - await Future.delayed( - const Duration(milliseconds: 100), - () async { - GoRouter.of(context).go( - '/mini-player', - extra: prevSize, - ); - }, - ); - }, - ), + final prevSize = await windowManager.getSize(); + await windowManager.setMinimumSize( + const Size(300, 300), + ); + await windowManager.setAlwaysOnTop(true); + if (!kIsLinux) { + await windowManager.setHasShadow(false); + } + await windowManager + .setAlignment(Alignment.topRight); + await windowManager.setSize(const Size(400, 500)); + await Future.delayed( + const Duration(milliseconds: 100), + () async { + GoRouter.of(context).go( + '/mini-player', + extra: prevSize, + ); + }, + ); + }, + ), ], ), Container( diff --git a/lib/pages/getting_started/sections/support.dart b/lib/pages/getting_started/sections/support.dart index b449def5..3f669557 100644 --- a/lib/pages/getting_started/sections/support.dart +++ b/lib/pages/getting_started/sections/support.dart @@ -6,7 +6,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/pages/home/home.dart'; -import 'package:spotube/pages/mobile_login/mobile_login.dart'; +import 'package:spotube/pages/mobile_login/hooks/login_callback.dart'; import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -16,6 +16,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final ThemeData(:textTheme, :colorScheme) = Theme.of(context); + final onLogin = useLoginCallback(ref); return Center( child: Column( @@ -121,9 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget { ), onPressed: () async { await KVStoreService.setDoneGettingStarted(true); - if (context.mounted) { - context.pushNamed(WebViewLogin.name); - } + await onLogin(); }, ), ], diff --git a/lib/pages/lyrics/lyrics.dart b/lib/pages/lyrics/lyrics.dart index a81e3ba6..423212f3 100644 --- a/lib/pages/lyrics/lyrics.dart +++ b/lib/pages/lyrics/lyrics.dart @@ -6,7 +6,6 @@ import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/collections/spotube_icons.dart'; -import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/themed_button_tab_bar.dart'; @@ -17,7 +16,6 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/utils/use_palette_color.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:spotube/provider/spotify/spotify.dart'; @@ -82,15 +80,6 @@ class LyricsPage extends HookConsumerWidget { ), ); - final auth = ref.watch(authenticationProvider); - - if (auth.asData?.value == null) { - return Scaffold( - appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null, - body: const AnonymousFallback(), - ); - } - if (isModal) { return DefaultTabController( length: 2, diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index dbff563d..8f6ec1fc 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -8,13 +8,10 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/root/sidebar.dart'; -import 'package:spotube/components/fallbacks/anonymous_fallback.dart'; -import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/synced_lyrics.dart'; -import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -46,14 +43,7 @@ class MiniLyricsPage extends HookConsumerWidget { return null; }, []); - final auth = ref.watch(authenticationProvider); - - if (auth.asData?.value == null) { - return const Scaffold( - appBar: PageWindowTitleBar(), - body: AnonymousFallback(), - ); - } + return MouseRegion( onEnter: !hoverMode.value diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 643c1064..59bd863a 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -17,6 +17,7 @@ import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:stroke_text/stroke_text.dart'; @@ -80,12 +81,16 @@ class SyncedLyrics extends HookConsumerWidget { StreamSubscription? subscription; WidgetsBinding.instance.addPostFrameCallback((_) { subscription = audioPlayer.positionStream.listen((event) { - if (event > Duration.zero) return; - controller.animateTo( - 0, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); + try { + if (event > Duration.zero || !controller.hasClients) return; + controller.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }); }); diff --git a/lib/pages/mobile_login/hooks/login_callback.dart b/lib/pages/mobile_login/hooks/login_callback.dart new file mode 100644 index 00000000..1648da19 --- /dev/null +++ b/lib/pages/mobile_login/hooks/login_callback.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:spotube/pages/mobile_login/mobile_login.dart'; +import 'package:spotube/pages/mobile_login/no_webview_runtime_dialog.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; +import 'package:spotube/utils/platform.dart'; + +Future Function() useLoginCallback(WidgetRef ref) { + final context = useContext(); + final theme = Theme.of(context); + final authNotifier = ref.read(authenticationProvider.notifier); + + return useCallback(() async { + if (kIsMobile) { + context.pushNamed(WebViewLogin.name); + return; + } + + try { + final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); + final applicationSupportDir = await getApplicationSupportDirectory(); + final userDataFolder = Directory( + join(applicationSupportDir.path, "webview_window_Webview2")); + + if (!await userDataFolder.exists()) { + await userDataFolder.create(); + } + + final webview = await WebviewWindow.create( + configuration: CreateConfiguration( + title: "Spotify Login", + titleBarTopPadding: kIsMacOS ? 20 : 0, + windowHeight: 720, + windowWidth: 1280, + userDataFolderWindows: userDataFolder.path, + ), + ); + webview + ..setBrightness(theme.colorScheme.brightness) + ..launch("https://accounts.spotify.com/") + ..setOnUrlRequestCallback((url) { + if (exp.hasMatch(url)) { + webview.getAllCookies().then((cookies) async { + final cookieHeader = + "sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}"; + + await authNotifier.login(cookieHeader); + + webview.close(); + if (context.mounted) { + context.go("/"); + } + }); + } + + return true; + }); + } on PlatformException catch (_) { + if (!await WebviewWindow.isWebviewAvailable()) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showDialog( + context: context, + builder: (context) { + return const NoWebviewRuntimeDialog(); + }, + ); + }); + } + } + }, [authNotifier, theme, context.go, context.pushNamed]); +} diff --git a/lib/pages/mobile_login/mobile_login.dart b/lib/pages/mobile_login/mobile_login.dart index 290c2b2f..10a989cf 100644 --- a/lib/pages/mobile_login/mobile_login.dart +++ b/lib/pages/mobile_login/mobile_login.dart @@ -27,7 +27,7 @@ class WebViewLogin extends HookConsumerWidget { child: InAppWebView( initialSettings: InAppWebViewSettings( userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 afari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36", ), initialUrlRequest: URLRequest( url: WebUri("https://accounts.spotify.com/"), diff --git a/lib/pages/mobile_login/no_webview_runtime_dialog.dart b/lib/pages/mobile_login/no_webview_runtime_dialog.dart new file mode 100644 index 00000000..a6cc5ffb --- /dev/null +++ b/lib/pages/mobile_login/no_webview_runtime_dialog.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class NoWebviewRuntimeDialog extends StatelessWidget { + const NoWebviewRuntimeDialog({super.key}); + + @override + Widget build(BuildContext context) { + final ThemeData(:platform) = Theme.of(context); + + return AlertDialog( + title: Text(context.l10n.webview_not_found), + content: Text(context.l10n.webview_not_found_description), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.l10n.cancel), + ), + FilledButton( + onPressed: () async { + final url = switch (platform) { + TargetPlatform.windows => + 'https://developer.microsoft.com/en-us/microsoft-edge/webview2', + TargetPlatform.macOS => 'https://www.apple.com/safari/', + TargetPlatform.linux => + 'https://webkitgtk.org/reference/webkit2gtk/stable/', + _ => "", + }; + if (url.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Unsupported platform')), + ); + } + + await launchUrlString(url); + }, + child: Text(switch (platform) { + TargetPlatform.windows => 'Download Edge WebView2', + TargetPlatform.macOS => 'Download Safari', + TargetPlatform.linux => 'Download Webkit2Gtk', + _ => 'Download Webview', + }), + ), + ], + ); + } +} diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index f7aedf63..0274de00 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -5,7 +5,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/components/framework/app_pop_scope.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/modules/root/bottom_player.dart'; @@ -30,10 +32,11 @@ class RootApp extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final theme = Theme.of(context); + final showingDialogCompleter = useRef(Completer()..complete()); final downloader = ref.watch(downloadManagerProvider); final scaffoldMessenger = ScaffoldMessenger.of(context); - final theme = Theme.of(context); final connectRoutes = ref.watch(serverConnectRoutesProvider); useEffect(() { @@ -164,55 +167,69 @@ class RootApp extends HookConsumerWidget { return null; }, [backgroundColor]); - // ignore: deprecated_member_use - return WillPopScope( - onWillPop: () async { - final routerState = GoRouterState.of(context); - if (routerState.matchedLocation != "/") { - context.goNamed(HomePage.name); - return false; - } - return true; - }, - child: Scaffold( - body: Sidebar(child: child), - extendBody: true, - drawerScrimColor: Colors.transparent, - endDrawer: kIsDesktop - ? Container( - constraints: const BoxConstraints(maxWidth: 800), - decoration: BoxDecoration( - boxShadow: theme.brightness == Brightness.light - ? null - : kElevationToShadow[8], - ), - margin: const EdgeInsets.only( - top: 40, - bottom: 100, - ), - child: Consumer( - builder: (context, ref, _) { - final playlist = ref.watch(audioPlayerProvider); - final playlistNotifier = - ref.read(audioPlayerProvider.notifier); + final navTileNames = useMemoized(() { + return getSidebarTileList(context.l10n).map((s) => s.name).toList(); + }, []); - return PlayerQueue.fromAudioPlayerNotifier( - floating: true, - playlist: playlist, - notifier: playlistNotifier, - ); - }, - ), - ) - : null, - bottomNavigationBar: const Column( - mainAxisSize: MainAxisSize.min, - children: [ - BottomPlayer(), - SpotubeNavigationBar(), - ], - ), + final scaffold = Scaffold( + body: Sidebar(child: child), + extendBody: true, + drawerScrimColor: Colors.transparent, + endDrawer: kIsDesktop + ? Container( + constraints: const BoxConstraints(maxWidth: 800), + decoration: BoxDecoration( + boxShadow: theme.brightness == Brightness.light + ? null + : kElevationToShadow[8], + ), + margin: const EdgeInsets.only( + top: 40, + bottom: 100, + ), + child: Consumer( + builder: (context, ref, _) { + final playlist = ref.watch(audioPlayerProvider); + final playlistNotifier = + ref.read(audioPlayerProvider.notifier); + + return PlayerQueue.fromAudioPlayerNotifier( + floating: true, + playlist: playlist, + notifier: playlistNotifier, + ); + }, + ), + ) + : null, + bottomNavigationBar: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + BottomPlayer(), + SpotubeNavigationBar(), + ], ), ); + + if (!kIsAndroid) { + return scaffold; + } + + final topRoute = GoRouterState.of(context).topRoute; + final canPop = topRoute != null && !navTileNames.contains(topRoute.name); + + return AppPopScope( + canPop: canPop, + onPopInvoked: (didPop) { + if (didPop) return; + + if (topRoute?.name == HomePage.name) { + SystemNavigator.pop(); + } else { + context.goNamed(HomePage.name); + } + }, + child: scaffold, + ); } } diff --git a/lib/pages/settings/logs.dart b/lib/pages/settings/logs.dart index 91087b7e..6ccbe32f 100644 --- a/lib/pages/settings/logs.dart +++ b/lib/pages/settings/logs.dart @@ -7,6 +7,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/logs/logs_provider.dart'; +import 'package:spotube/services/logger/logger.dart'; class LogsPage extends HookConsumerWidget { static const name = "logs"; @@ -40,6 +41,17 @@ class LogsPage extends HookConsumerWidget { } }, ), + IconButton( + icon: const Icon(SpotubeIcons.trash), + iconSize: 16, + onPressed: () async { + ref.invalidate(logsProvider); + + final logsFile = await AppLogger.getLogsPath(); + + await logsFile.writeAsString(""); + }, + ) ], ), body: SafeArea( diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index c670e96d..b9a26147 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -1,24 +1,18 @@ -import 'dart:io'; - import 'package:auto_size_text/auto_size_text.dart'; -import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; -import 'package:spotube/pages/mobile_login/mobile_login.dart'; import 'package:spotube/pages/profile/profile.dart'; +import 'package:spotube/pages/mobile_login/hooks/login_callback.dart'; import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/service_utils.dart'; class SettingsAccountSection extends HookConsumerWidget { @@ -30,7 +24,6 @@ class SettingsAccountSection extends HookConsumerWidget { final router = GoRouter.of(context); final auth = ref.watch(authenticationProvider); - final authNotifier = ref.watch(authenticationProvider.notifier); final scrobbler = ref.watch(scrobblerProvider); final me = ref.watch(meProvider); final meData = me.asData?.value; @@ -40,51 +33,7 @@ class SettingsAccountSection extends HookConsumerWidget { foregroundColor: Colors.white, ); - void onLogin() async { - if (kIsMobile) { - router.pushNamed(WebViewLogin.name); - return; - } - - final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status"); - final applicationSupportDir = await getApplicationSupportDirectory(); - final userDataFolder = Directory( - join(applicationSupportDir.path, "webview_window_Webview2")); - - if (!await userDataFolder.exists()) { - await userDataFolder.create(); - } - - final webview = await WebviewWindow.create( - configuration: CreateConfiguration( - title: "Spotify Login", - titleBarTopPadding: kIsMacOS ? 20 : 0, - windowHeight: 720, - windowWidth: 1280, - userDataFolderWindows: userDataFolder.path, - ), - ); - webview - ..setBrightness(theme.colorScheme.brightness) - ..launch("https://accounts.spotify.com/") - ..setOnUrlRequestCallback((url) { - if (exp.hasMatch(url)) { - webview.getAllCookies().then((cookies) async { - final cookieHeader = - "sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}"; - - await authNotifier.login(cookieHeader); - - webview.close(); - if (context.mounted) { - context.go("/"); - } - }); - } - - return true; - }); - } + final onLogin = useLoginCallback(ref); return SectionCardWithHeading( heading: context.l10n.account, diff --git a/lib/pages/stats/minutes/minutes.dart b/lib/pages/stats/minutes/minutes.dart index 35bea3ab..3ad0984b 100644 --- a/lib/pages/stats/minutes/minutes.dart +++ b/lib/pages/stats/minutes/minutes.dart @@ -49,8 +49,8 @@ class StatsMinutesPage extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - context.l10n - .count_plays(compactNumberFormatter.format(track.count)), + context.l10n.count_mins(compactNumberFormatter + .format(track.count * track.track.duration!.inMinutes)), ), ); }, diff --git a/lib/pages/stats/streams/streams.dart b/lib/pages/stats/streams/streams.dart index 5c90e879..059366e0 100644 --- a/lib/pages/stats/streams/streams.dart +++ b/lib/pages/stats/streams/streams.dart @@ -49,8 +49,8 @@ class StatsStreamsPage extends HookConsumerWidget { return StatsTrackItem( track: track.track, info: Text( - context.l10n.count_mins(compactNumberFormatter - .format(track.count * track.track.duration!.inMinutes)), + context.l10n + .count_plays(compactNumberFormatter.format(track.count)), ), ); }, diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 6f3af0e4..84c53b74 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -14,6 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; +import 'package:spotube/extensions/list.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -167,7 +168,8 @@ class TrackPage extends HookConsumerWidget { children: [ const Gap(5), if (!isActive && - !playlist.tracks.contains(track)) + !playlist.tracks + .containsBy(track, (t) => t.id)) OutlinedButton.icon( icon: const Icon(SpotubeIcons.queueAdd), label: Text(context.l10n.queue), @@ -177,7 +179,8 @@ class TrackPage extends HookConsumerWidget { ), const Gap(5), if (!isActive && - !playlist.tracks.contains(track)) + !playlist.tracks + .containsBy(track, (t) => t.id)) IconButton.outlined( icon: const Icon(SpotubeIcons.lightning), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index c40f683d..7c1b6897 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:spotify/spotify.dart' hide Playlist; +import 'package:spotube/extensions/list.dart'; import 'package:spotube/extensions/track.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/local_track.dart'; @@ -13,6 +14,7 @@ import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; class AudioPlayerNotifier extends Notifier { BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); @@ -141,36 +143,52 @@ class AudioPlayerNotifier extends Notifier { build() { final subscriptions = [ audioPlayer.playingStream.listen((playing) async { - state = state.copyWith(playing: playing); + try { + state = state.copyWith(playing: playing); - await _updatePlayerState( - AudioPlayerStateTableCompanion( - playing: Value(playing), - ), - ); + await _updatePlayerState( + AudioPlayerStateTableCompanion( + playing: Value(playing), + ), + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }), audioPlayer.loopModeStream.listen((loopMode) async { - state = state.copyWith(loopMode: loopMode); + try { + state = state.copyWith(loopMode: loopMode); - await _updatePlayerState( - AudioPlayerStateTableCompanion( - loopMode: Value(loopMode), - ), - ); + await _updatePlayerState( + AudioPlayerStateTableCompanion( + loopMode: Value(loopMode), + ), + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }), audioPlayer.shuffledStream.listen((shuffled) async { - state = state.copyWith(shuffled: shuffled); + try { + state = state.copyWith(shuffled: shuffled); - await _updatePlayerState( - AudioPlayerStateTableCompanion( - shuffled: Value(shuffled), - ), - ); + await _updatePlayerState( + AudioPlayerStateTableCompanion( + shuffled: Value(shuffled), + ), + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }), audioPlayer.playlistStream.listen((playlist) async { - state = state.copyWith(playlist: playlist); + try { + state = state.copyWith(playlist: playlist); - await _updatePlaylist(playlist); + await _updatePlaylist(playlist); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }), ]; @@ -239,6 +257,10 @@ class AudioPlayerNotifier extends Notifier { for (int i = 0; i < tracks.length; i++) { final track = tracks.elementAt(i); + if (state.tracks.any((element) => _compareTracks(element, track))) { + continue; + } + await audioPlayer.addTrackAt( SpotubeMedia(track), max(state.playlist.index, 0) + i + 1, @@ -248,6 +270,7 @@ class AudioPlayerNotifier extends Notifier { Future addTrack(Track track) async { if (_blacklist.contains(track)) return; + if (state.tracks.any((element) => _compareTracks(element, track))) return; await audioPlayer.addTrack(SpotubeMedia(track)); } @@ -272,13 +295,23 @@ class AudioPlayerNotifier extends Notifier { } } + bool _compareTracks(Track a, Track b) { + if ((a is LocalTrack && b is! LocalTrack) || + (a is! LocalTrack && b is LocalTrack)) return false; + + return a is LocalTrack && b is LocalTrack + ? (a).path == (b).path + : a.id == b.id; + } + Future load( List tracks, { int initialIndex = 0, bool autoPlay = false, }) async { - final medias = - (_blacklist.filter(tracks).toList() as List).asMediaList(); + final medias = (_blacklist.filter(tracks).toList() as List) + .asMediaList() + .unique((a, b) => _compareTracks(a.track, b.track)); // Giving the initial track a boost so MediaKit won't skip // because of timeout diff --git a/lib/provider/audio_player/audio_player_streams.dart b/lib/provider/audio_player/audio_player_streams.dart index 845f12ea..08550844 100644 --- a/lib/provider/audio_player/audio_player_streams.dart +++ b/lib/provider/audio_player/audio_player_streams.dart @@ -73,25 +73,33 @@ class AudioPlayerStreamListeners { StreamSubscription subscribeToPlaylist() { return audioPlayer.playlistStream.listen((mpvPlaylist) { - notificationService.addTrack(audioPlayerState.activeTrack!); - discord.updatePresence(audioPlayerState.activeTrack!); - updatePalette(); + try { + notificationService.addTrack(audioPlayerState.activeTrack!); + discord.updatePresence(audioPlayerState.activeTrack!); + updatePalette(); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }); } StreamSubscription subscribeToSkipSponsor() { return audioPlayer.positionStream.listen((position) async { - final currentSegments = await ref.read(segmentProvider.future); + try { + final currentSegments = await ref.read(segmentProvider.future); - if (currentSegments?.segments.isNotEmpty != true || - position < const Duration(seconds: 3)) return; + if (currentSegments?.segments.isNotEmpty != true || + position < const Duration(seconds: 3)) return; - for (final segment in currentSegments!.segments) { - final seconds = position.inSeconds; + for (final segment in currentSegments!.segments) { + final seconds = position.inSeconds; - if (seconds < segment.start || seconds >= segment.end) continue; + if (seconds < segment.start || seconds >= segment.end) continue; - await audioPlayer.seek(Duration(seconds: segment.end + 1)); + await audioPlayer.seek(Duration(seconds: segment.end + 1)); + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); } @@ -122,23 +130,28 @@ class AudioPlayerStreamListeners { StreamSubscription subscribeToPosition() { String lastTrack = ""; // used to prevent multiple calls to the same track return audioPlayer.positionStream.listen((event) async { - if (event < const Duration(seconds: 3) || - audioPlayerState.playlist.index == -1 || - audioPlayerState.playlist.index == - audioPlayerState.tracks.length - 1) { - return; - } - final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias - .elementAt(audioPlayerState.playlist.index + 1)); - - if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { - return; - } - try { - await ref.read(sourcedTrackProvider(nextTrack).future); - } finally { - lastTrack = nextTrack.track.id!; + if (event < const Duration(seconds: 3) || + audioPlayerState.playlist.index == -1 || + audioPlayerState.playlist.index == + audioPlayerState.tracks.length - 1) { + return; + } + final nextTrack = SpotubeMedia.fromMedia(audioPlayerState + .playlist.medias + .elementAt(audioPlayerState.playlist.index + 1)); + + if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { + return; + } + + try { + await ref.read(sourcedTrackProvider(nextTrack).future); + } finally { + lastTrack = nextTrack.track.id!; + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); } diff --git a/lib/provider/connect/clients.dart b/lib/provider/connect/clients.dart index d92ff8d3..51578a7b 100644 --- a/lib/provider/connect/clients.dart +++ b/lib/provider/connect/clients.dart @@ -1,6 +1,7 @@ import 'package:bonsoir/bonsoir.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/services/device_info/device_info.dart'; +import 'package:spotube/services/logger/logger.dart'; class ConnectClientsState { final List services; @@ -37,42 +38,47 @@ class ConnectClientsNotifier extends AsyncNotifier { final subscription = discovery.eventStream?.listen((event) { // ignore device itself - if (event.service?.attributes["deviceId"] == deviceId) { - return; - } + try { + if (event.service?.attributes["deviceId"] == deviceId) { + return; + } - switch (event.type) { - case BonsoirDiscoveryEventType.discoveryServiceFound: - state = AsyncData(state.value!.copyWith( - services: [ - ...?state.value?.services, - event.service!, - ], - )); - break; - case BonsoirDiscoveryEventType.discoveryServiceResolved: - state = AsyncData( - state.value!.copyWith( - resolvedService: event.service as ResolvedBonsoirService, - ), - ); - break; - case BonsoirDiscoveryEventType.discoveryServiceLost: - state = AsyncData( - ConnectClientsState( - services: state.value!.services - .where((s) => s.name != event.service!.name) - .toList(), - discovery: state.value!.discovery, - resolvedService: state.value?.resolvedService != null && - event.service?.name == state.value?.resolvedService?.name - ? null - : state.value!.resolvedService, - ), - ); - break; - default: - break; + switch (event.type) { + case BonsoirDiscoveryEventType.discoveryServiceFound: + state = AsyncData(state.value!.copyWith( + services: [ + ...?state.value?.services, + event.service!, + ], + )); + break; + case BonsoirDiscoveryEventType.discoveryServiceResolved: + state = AsyncData( + state.value!.copyWith( + resolvedService: event.service as ResolvedBonsoirService, + ), + ); + break; + case BonsoirDiscoveryEventType.discoveryServiceLost: + state = AsyncData( + ConnectClientsState( + services: state.value!.services + .where((s) => s.name != event.service!.name) + .toList(), + discovery: state.value!.discovery, + resolvedService: state.value?.resolvedService != null && + event.service?.name == + state.value?.resolvedService?.name + ? null + : state.value!.resolvedService, + ), + ); + break; + default: + break; + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); diff --git a/lib/provider/discord_provider.dart b/lib/provider/discord_provider.dart index 8f8cb375..8f81fc51 100644 --- a/lib/provider/discord_provider.dart +++ b/lib/provider/discord_provider.dart @@ -7,11 +7,14 @@ import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/utils/platform.dart'; class DiscordNotifier extends AsyncNotifier { @override FutureOr build() async { + if (!kIsDesktop) return; + final enabled = ref.watch( userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop)); @@ -19,26 +22,38 @@ class DiscordNotifier extends AsyncNotifier { final subscriptions = [ FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { - final playback = ref.read(audioPlayerProvider); - if (connected && playback.activeTrack != null) { - await updatePresence(playback.activeTrack!); + try { + final playback = ref.read(audioPlayerProvider); + if (connected && playback.activeTrack != null) { + await updatePresence(playback.activeTrack!); + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }), audioPlayer.playerStateStream.listen((state) async { - final playback = ref.read(audioPlayerProvider); - if (playback.activeTrack == null) return; + try { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack == null) return; - await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + } catch (e, stack) { + AppLogger.reportError(e, stack); + } }), audioPlayer.positionStream.listen((position) async { - final playback = ref.read(audioPlayerProvider); - if (playback.activeTrack != null) { - final diff = position.inMilliseconds - lastPosition.inMilliseconds; - if (diff > 500 || diff < -500) { - await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + try { + final playback = ref.read(audioPlayerProvider); + if (playback.activeTrack != null) { + final diff = position.inMilliseconds - lastPosition.inMilliseconds; + if (diff > 500 || diff < -500) { + await updatePresence(ref.read(audioPlayerProvider).activeTrack!); + } } + lastPosition = position; + } catch (e, stack) { + AppLogger.reportError(e, stack); } - lastPosition = position; }) ]; @@ -46,6 +61,7 @@ class DiscordNotifier extends AsyncNotifier { for (final subscription in subscriptions) { subscription.cancel(); } + await clear(); await close(); await FlutterDiscordRPC.instance.dispose(); }); @@ -53,12 +69,14 @@ class DiscordNotifier extends AsyncNotifier { if (!enabled && FlutterDiscordRPC.instance.isConnected) { await clear(); await close(); - } else { + } else if (enabled) { await FlutterDiscordRPC.instance.connect(autoRetry: true); } } Future updatePresence(Track track) async { + if (!kIsDesktop) return; + if (FlutterDiscordRPC.instance.isConnected == false) return; final artistNames = track.artists?.asString(); final isPlaying = audioPlayer.isPlaying; final position = audioPlayer.position; @@ -92,10 +110,12 @@ class DiscordNotifier extends AsyncNotifier { } Future clear() async { + if (!kIsDesktop) return; await FlutterDiscordRPC.instance.clearActivity(); } Future close() async { + if (!kIsDesktop) return; await FlutterDiscordRPC.instance.disconnect(); } } diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index ec6ffc18..8c9ffadf 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -23,68 +23,72 @@ class DownloadManagerProvider extends ChangeNotifier { $backHistory = {}, dl = DownloadManager() { dl.statusStream.listen((event) async { - final (:request, :status) = event; + try { + final (:request, :status) = event; - final track = $history.firstWhereOrNull( - (element) => element.getUrlOfCodec(downloadCodec) == request.url, - ); - if (track == null) return; + final track = $history.firstWhereOrNull( + (element) => element.getUrlOfCodec(downloadCodec) == request.url, + ); + if (track == null) return; - final savePath = getTrackFileUrl(track); - // related to onFileExists - final oldFile = File("$savePath.old"); + final savePath = getTrackFileUrl(track); + // related to onFileExists + final oldFile = File("$savePath.old"); - // if download failed and old file exists, rename it back - if ((status == DownloadStatus.failed || - status == DownloadStatus.canceled) && - await oldFile.exists()) { - await oldFile.rename(savePath); + // if download failed and old file exists, rename it back + if ((status == DownloadStatus.failed || + status == DownloadStatus.canceled) && + await oldFile.exists()) { + await oldFile.rename(savePath); + } + if (status != DownloadStatus.completed || + //? WebA audiotagging is not supported yet + //? Although in future by converting weba to opus & then tagging it + //? is possible using vorbis comments + downloadCodec == SourceCodecs.weba) return; + + final file = File(request.path); + + if (await oldFile.exists()) { + await oldFile.delete(); + } + + final imageBytes = await downloadImage( + (track.album?.images).asUrlString( + placeholder: ImagePlaceholder.albumArt, + index: 1, + ), + ); + + final metadata = Metadata( + title: track.name, + artist: track.artists?.map((a) => a.name).join(", "), + album: track.album?.name, + albumArtist: track.artists?.map((a) => a.name).join(", "), + year: track.album?.releaseDate != null + ? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969 + : 1969, + trackNumber: track.trackNumber, + discNumber: track.discNumber, + durationMs: track.durationMs?.toDouble() ?? 0.0, + fileSize: BigInt.from(await file.length()), + trackTotal: track.album?.tracks?.length ?? 0, + picture: imageBytes != null + ? Picture( + data: imageBytes, + // Spotify images are always JPEGs + mimeType: 'image/jpeg', + ) + : null, + ); + + await MetadataGod.writeMetadata( + file: file.path, + metadata: metadata, + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); } - if (status != DownloadStatus.completed || - //? WebA audiotagging is not supported yet - //? Although in future by converting weba to opus & then tagging it - //? is possible using vorbis comments - downloadCodec == SourceCodecs.weba) return; - - final file = File(request.path); - - if (await oldFile.exists()) { - await oldFile.delete(); - } - - final imageBytes = await downloadImage( - (track.album?.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - index: 1, - ), - ); - - final metadata = Metadata( - title: track.name, - artist: track.artists?.map((a) => a.name).join(", "), - album: track.album?.name, - albumArtist: track.artists?.map((a) => a.name).join(", "), - year: track.album?.releaseDate != null - ? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969 - : 1969, - trackNumber: track.trackNumber, - discNumber: track.discNumber, - durationMs: track.durationMs?.toDouble() ?? 0.0, - fileSize: BigInt.from(await file.length()), - trackTotal: track.album?.tracks?.length ?? 0, - picture: imageBytes != null - ? Picture( - data: imageBytes, - // Spotify images are always JPEGs - mimeType: 'image/jpeg', - ) - : null, - ); - - await MetadataGod.writeMetadata( - file: file.path, - metadata: metadata, - ); }); } diff --git a/lib/provider/history/top/albums.dart b/lib/provider/history/top/albums.dart index 7448a849..b11e62d2 100644 --- a/lib/provider/history/top/albums.dart +++ b/lib/provider/history/top/albums.dart @@ -90,12 +90,18 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); - return getAlbumsWithCount(await albumsQuery.get()); + final items = getAlbumsWithCount(await albumsQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final albums = await fetch(arg, 0, 20); + final (items: albums, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createAlbumsQuery().watch().listen((event) { if (state.asData == null) return; @@ -111,9 +117,9 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopAlbumsState( items: albums, - offset: albums.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/history/top/playlists.dart b/lib/provider/history/top/playlists.dart index 04071f7a..19eb3622 100644 --- a/lib/provider/history/top/playlists.dart +++ b/lib/provider/history/top/playlists.dart @@ -55,12 +55,18 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); - return getPlaylistsWithCount(await playlistsQuery.get()); + final items = getPlaylistsWithCount(await playlistsQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final playlists = await fetch(arg, 0, 20); + final (items: playlists, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createPlaylistsQuery().watch().listen((event) { if (state.asData == null) return; @@ -76,9 +82,9 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopPlaylistsState( items: playlists, - offset: playlists.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/history/top/tracks.dart b/lib/provider/history/top/tracks.dart index 56795cc6..b737d148 100644 --- a/lib/provider/history/top/tracks.dart +++ b/lib/provider/history/top/tracks.dart @@ -89,12 +89,18 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< fetch(arg, offset, limit) async { final tracksQuery = createTracksQuery()..limit(limit, offset: offset); - return getTracksWithCount(await tracksQuery.get()); + final items = getTracksWithCount(await tracksQuery.get()); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override build(arg) async { - final tracks = await fetch(arg, 0, 20); + final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); final subscription = createTracksQuery().watch().listen((event) { if (state.asData == null) return; @@ -110,9 +116,9 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier< return HistoryTopTracksState( items: tracks, - offset: tracks.length, + offset: nextOffset, limit: 20, - hasMore: true, + hasMore: hasMore, ); } diff --git a/lib/provider/local_tracks/local_tracks_provider.dart b/lib/provider/local_tracks/local_tracks_provider.dart index ca22d841..513fd9b9 100644 --- a/lib/provider/local_tracks/local_tracks_provider.dart +++ b/lib/provider/local_tracks/local_tracks_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -72,39 +73,35 @@ final localTracksProvider = } } - final List> filesWithMetadata = []; + final List> filesWithMetadata = await Future.wait( + entities.map((file) async { + try { + final metadata = await MetadataGod.readMetadata(file: file.path); - for (final file in entities) { - try { - final metadata = await MetadataGod.readMetadata(file: file.path); + final imageFile = File(join( + (await getTemporaryDirectory()).path, + "spotube", + basenameWithoutExtension(file.path) + + imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, + )); + if (!await imageFile.exists() && metadata.picture != null) { + await imageFile.create(recursive: true); + await imageFile.writeAsBytes( + metadata.picture?.data ?? [], + mode: FileMode.writeOnly, + ); + } - await Future.delayed(const Duration(milliseconds: 50)); - - final imageFile = File(join( - (await getTemporaryDirectory()).path, - "spotube", - basenameWithoutExtension(file.path) + - imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!, - )); - if (!await imageFile.exists() && metadata.picture != null) { - await imageFile.create(recursive: true); - await imageFile.writeAsBytes( - metadata.picture?.data ?? [], - mode: FileMode.writeOnly, - ); + return {"metadata": metadata, "file": file, "art": imageFile.path}; + } catch (e, stack) { + if (e case FrbException() || TimeoutException()) { + return {"file": file}; + } + AppLogger.reportError(e, stack); + return null; } - - filesWithMetadata.add( - {"metadata": metadata, "file": file, "art": imageFile.path}, - ); - } catch (e, stack) { - if (e case FrbException() || TimeoutException()) { - filesWithMetadata.add({"file": file}); - } - AppLogger.reportError(e, stack); - continue; - } - } + }), + ).then((value) => value.whereNotNull().toList()); final tracksFromMetadata = filesWithMetadata .map( diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart index 76559d69..8aff0438 100644 --- a/lib/provider/scrobbler/scrobbler.dart +++ b/lib/provider/scrobbler/scrobbler.dart @@ -23,19 +23,23 @@ class ScrobblerNotifier extends AsyncNotifier { final subscription = database.select(database.scrobblerTable).watch().listen((event) async { - if (event.isNotEmpty) { - state = await AsyncValue.guard( - () async => Scrobblenaut( - lastFM: await LastFM.authenticateWithPasswordHash( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: event.first.username, - passwordHash: event.first.passwordHash.value, + try { + if (event.isNotEmpty) { + state = await AsyncValue.guard( + () async => Scrobblenaut( + lastFM: await LastFM.authenticateWithPasswordHash( + apiKey: Env.lastFmApiKey, + apiSecret: Env.lastFmApiSecret, + username: event.first.username, + passwordHash: event.first.passwordHash.value, + ), ), - ), - ); - } else { - state = const AsyncValue.data(null); + ); + } else { + state = const AsyncValue.data(null); + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index e9f712e7..e39abad5 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier e.asTrack(arg)).toList() ?? []; + final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; + + return ( + items: items, + hasMore: !tracks.isLast, + nextOffset: tracks.nextOffset, + ); } @override @@ -39,12 +45,12 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier s.market), ); - final albums = await fetch(arg, 0, 20); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 20); return ArtistAlbumsState( - items: albums, - offset: 0, + items: items, + offset: nextOffset, limit: 20, - hasMore: albums.length == 20, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/category/playlists.dart b/lib/provider/spotify/category/playlists.dart index 18d4845f..9f1034be 100644 --- a/lib/provider/spotify/category/playlists.dart +++ b/lib/provider/spotify/category/playlists.dart @@ -39,7 +39,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< (json) => PlaylistsFeatured.fromJson(json), ).getPage(limit, offset); - return playlists.items?.whereNotNull().toList() ?? []; + final items = playlists.items?.whereNotNull().toList() ?? []; + + return ( + items: items, + hasMore: !playlists.isLast, + nextOffset: playlists.nextOffset, + ); } @override @@ -50,13 +56,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.watch(userPreferencesProvider.select((s) => s.locale)); ref.watch(userPreferencesProvider.select((s) => s.market)); - final playlists = await fetch(arg, 0, 8); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 8); return CategoryPlaylistsState( - items: playlists, - offset: 0, + items: items, + offset: nextOffset, limit: 8, - hasMore: playlists.length == 8, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/lyrics/synced.dart b/lib/provider/spotify/lyrics/synced.dart index 085fccb7..c6c0d6e3 100644 --- a/lib/provider/spotify/lyrics/synced.dart +++ b/lib/provider/spotify/lyrics/synced.dart @@ -125,6 +125,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { try { final database = ref.watch(databaseProvider); final spotify = ref.watch(spotifyProvider); + final auth = await ref.watch(authenticationProvider.future); if (track == null) { throw "No track currently"; @@ -139,11 +140,13 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier { final token = await spotify.getCredentials(); - if (lyrics == null || lyrics.lyrics.isEmpty) { + if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) { lyrics = await getSpotifyLyrics(token.accessToken); } - if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) { + if (lyrics == null || + lyrics.lyrics.isEmpty || + lyrics.lyrics.length <= 5) { lyrics = await getLRCLibLyrics(); } diff --git a/lib/provider/spotify/playlist/tracks.dart b/lib/provider/spotify/playlist/tracks.dart index 1803f6fc..379ad110 100644 --- a/lib/provider/spotify/playlist/tracks.dart +++ b/lib/provider/spotify/playlist/tracks.dart @@ -36,10 +36,16 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< /// Filter out tracks with null id because some personal playlists /// may contain local tracks that are not available in the Spotify catalog - return tracks.items + final items = tracks.items ?.where((track) => track.id != null && track.type == "track") .toList() ?? []; + + return ( + items: items, + hasMore: !tracks.isLast, + nextOffset: tracks.nextOffset, + ); } @override @@ -47,13 +53,13 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier< ref.cacheFor(); ref.watch(spotifyProvider); - final tracks = await fetch(arg, 0, 20); + final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20); return PlaylistTracksState( items: tracks, - offset: 0, + offset: nextOffset, limit: 20, - hasMore: tracks.length == 20, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/search/search.dart b/lib/provider/spotify/search/search.dart index dc00d913..5bbc02e4 100644 --- a/lib/provider/spotify/search/search.dart +++ b/lib/provider/spotify/search/search.dart @@ -37,7 +37,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier[], + hasMore: false, + nextOffset: 0, + ); + } final results = await spotify.search .get( ref.read(searchTermStateProvider), @@ -46,7 +52,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier e.items ?? []).toList().cast(); + final items = results.expand((e) => e.items ?? []).toList().cast(); + + return ( + items: items, + hasMore: items.length == limit, + nextOffset: offset + limit, + ); } @override @@ -59,13 +71,13 @@ class SearchNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier value.market), ); - final results = await fetch(arg, 0, 10); + final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10); return SearchState( - items: results, - offset: 0, + items: items, + offset: nextOffset, limit: 10, - hasMore: results.length == 10, + hasMore: hasMore, ); } } diff --git a/lib/provider/spotify/spotify.dart b/lib/provider/spotify/spotify.dart index 5997a47a..8cf60120 100644 --- a/lib/provider/spotify/spotify.dart +++ b/lib/provider/spotify/spotify.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/spotify/utils/json_cast.dart'; import 'package:spotube/services/logger/logger.dart'; diff --git a/lib/provider/spotify/utils/provider/paginated_family.dart b/lib/provider/spotify/utils/provider/paginated_family.dart index 84c6ba20..c08c8673 100644 --- a/lib/provider/spotify/utils/provider/paginated_family.dart +++ b/lib/provider/spotify/utils/provider/paginated_family.dart @@ -1,10 +1,16 @@ part of '../../spotify.dart'; +typedef PseudoPaginatedProps = ({ + List items, + int nextOffset, + bool hasMore, +}); + abstract class FamilyPaginatedAsyncNotifier< K, T extends BasePaginatedState, A> extends FamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); + Future> fetch(A arg, int offset, int limit); Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; @@ -13,18 +19,18 @@ abstract class FamilyPaginatedAsyncNotifier< state = await AsyncValue.guard( () async { - final items = await fetch( + final (:items, :hasMore, :nextOffset) = await fetch( arg, - state.value!.offset + state.value!.limit, + state.value!.offset, state.value!.limit, ); return state.value!.copyWith( - hasMore: items.length == state.value!.limit, + hasMore: hasMore, items: [ ...state.value!.items, ...items, ], - offset: state.value!.offset + state.value!.limit, + offset: nextOffset, ) as T; }, ); @@ -37,16 +43,16 @@ abstract class FamilyPaginatedAsyncNotifier< bool hasMore = true; while (hasMore) { await update((state) async { - final items = await fetch( + final res = await fetch( arg, - state.offset + state.limit, + state.offset, state.limit, ); - hasMore = items.length == state.limit; + hasMore = res.hasMore; return state.copyWith( - items: [...state.items, ...items], - offset: state.offset + state.limit, + items: [...state.items, ...res.items], + offset: res.nextOffset, hasMore: hasMore, ) as T; }); @@ -60,7 +66,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< K, T extends BasePaginatedState, A> extends AutoDisposeFamilyAsyncNotifier with SpotifyMixin { - Future> fetch(A arg, int offset, int limit); + Future> fetch(A arg, int offset, int limit); Future fetchMore() async { if (state.value == null || !state.value!.hasMore) return; @@ -69,18 +75,19 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< state = await AsyncValue.guard( () async { - final items = await fetch( + final (:items, :hasMore, :nextOffset) = await fetch( arg, - state.value!.offset + state.value!.limit, + state.value!.offset, state.value!.limit, ); + return state.value!.copyWith( - hasMore: items.length == state.value!.limit, + hasMore: hasMore, items: [ ...state.value!.items, ...items, ], - offset: state.value!.offset + state.value!.limit, + offset: nextOffset, ) as T; }, ); @@ -93,16 +100,16 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier< bool hasMore = true; while (hasMore) { await update((state) async { - final items = await fetch( + final res = await fetch( arg, - state.offset + state.limit, + state.offset, state.limit, ); - hasMore = items.length == state.limit; + hasMore = res.hasMore; return state.copyWith( - items: [...state.items, ...items], - offset: state.offset + state.limit, + items: [...state.items, ...res.items], + offset: res.nextOffset, hasMore: hasMore, ) as T; }); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index a421e7d0..23479b71 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -10,6 +10,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -41,15 +42,21 @@ class UserPreferencesNotifier extends Notifier { ..where((tbl) => tbl.id.equals(0))) .watchSingle() .listen((event) async { - state = event; + try { + state = event; - if (kIsDesktop) { - await windowManager.setTitleBarStyle( - state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, - ); + if (kIsDesktop) { + await windowManager.setTitleBarStyle( + state.systemTitleBar + ? TitleBarStyle.normal + : TitleBarStyle.hidden, + ); + } + + await audioPlayer.setAudioNormalization(state.normalizeAudio); + } catch (e, stack) { + AppLogger.reportError(e, stack); } - - await audioPlayer.setAudioNormalization(state.normalizeAudio); }); ref.onDispose(() { diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index d1820a00..0b1843c4 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_services/mobile_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart'; @@ -30,8 +31,8 @@ class AudioServices with WidgetsBindingObserver { kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', androidNotificationOngoing: false, - androidNotificationIcon: "drawable/ic_launcher_monochrome", androidStopForegroundOnPause: false, + androidNotificationIcon: "drawable/ic_launcher_monochrome", androidNotificationChannelDescription: "Spotube Media Controls", ), ) @@ -73,7 +74,7 @@ class AudioServices with WidgetsBindingObserver { switch (state) { case AppLifecycleState.detached: deactivateSession(); - mobile?.stop(); + audioPlayer.pause(); break; default: break; diff --git a/lib/services/audio_services/mobile_audio_service.dart b/lib/services/audio_services/mobile_audio_service.dart index cdd16138..56fe0fc4 100644 --- a/lib/services/audio_services/mobile_audio_service.dart +++ b/lib/services/audio_services/mobile_audio_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; @@ -6,6 +7,8 @@ import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:media_kit/media_kit.dart' hide Track; +import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/utils/platform.dart'; class MobileAudioService extends BaseAudioHandler { AudioSession? session; @@ -119,36 +122,41 @@ class MobileAudioService extends BaseAudioHandler { @override Future onTaskRemoved() async { - await audioPlayerNotifier.stop(); - return super.onTaskRemoved(); + await audioPlayer.pause(); + if (kIsAndroid) exit(0); } Future _transformEvent() async { - return PlaybackState( - controls: [ - MediaControl.skipToPrevious, - audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play, - MediaControl.skipToNext, - MediaControl.stop, - ], - systemActions: { - MediaAction.seek, - }, - androidCompactActionIndices: const [0, 1, 2], - playing: audioPlayer.isPlaying, - updatePosition: audioPlayer.position, - bufferedPosition: audioPlayer.bufferedPosition, - shuffleMode: audioPlayer.isShuffled == true - ? AudioServiceShuffleMode.all - : AudioServiceShuffleMode.none, - repeatMode: switch (audioPlayer.loopMode) { - PlaylistMode.loop => AudioServiceRepeatMode.all, - PlaylistMode.single => AudioServiceRepeatMode.one, - _ => AudioServiceRepeatMode.none, - }, - processingState: audioPlayer.isBuffering - ? AudioProcessingState.loading - : AudioProcessingState.ready, - ); + try { + return PlaybackState( + controls: [ + MediaControl.skipToPrevious, + audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play, + MediaControl.skipToNext, + MediaControl.stop, + ], + systemActions: { + MediaAction.seek, + }, + androidCompactActionIndices: const [0, 1, 2], + playing: audioPlayer.isPlaying, + updatePosition: audioPlayer.position, + bufferedPosition: audioPlayer.bufferedPosition, + shuffleMode: audioPlayer.isShuffled == true + ? AudioServiceShuffleMode.all + : AudioServiceShuffleMode.none, + repeatMode: switch (audioPlayer.loopMode) { + PlaylistMode.loop => AudioServiceRepeatMode.all, + PlaylistMode.single => AudioServiceRepeatMode.one, + _ => AudioServiceRepeatMode.none, + }, + processingState: audioPlayer.isBuffering + ? AudioProcessingState.loading + : AudioProcessingState.ready, + ); + } catch (e, stack) { + AppLogger.reportError(e, stack); + rethrow; + } } } diff --git a/lib/services/connectivity_adapter.dart b/lib/services/connectivity_adapter.dart index 1a3835ee..86765671 100644 --- a/lib/services/connectivity_adapter.dart +++ b/lib/services/connectivity_adapter.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; +import 'package:spotube/services/logger/logger.dart'; class ConnectionCheckerService with WidgetsBindingObserver { final _connectionStreamController = StreamController.broadcast(); @@ -16,17 +17,21 @@ class ConnectionCheckerService with WidgetsBindingObserver { Timer? timer; onConnectivityChanged.listen((connected) { - if (!connected && timer == null) { - timer = Timer.periodic(const Duration(seconds: 30), (timer) async { - if (WidgetsBinding.instance.lifecycleState == - AppLifecycleState.paused) { - return; - } - await isConnected; - }); - } else { - timer?.cancel(); - timer = null; + try { + if (!connected && timer == null) { + timer = Timer.periodic(const Duration(seconds: 30), (timer) async { + if (WidgetsBinding.instance.lifecycleState == + AppLifecycleState.paused) { + return; + } + await isConnected; + }); + } else { + timer?.cancel(); + timer = null; + } + } catch (e, stack) { + AppLogger.reportError(e, stack); } }); } diff --git a/pubspec.lock b/pubspec.lock index 6f0f3e73..089563d8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -458,7 +458,7 @@ packages: source: hosted version: "1.2.0" dbus: - dependency: "direct main" + dependency: transitive description: name: dbus sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" @@ -506,14 +506,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - dots_indicator: - dependency: transitive - description: - name: dots_indicator - sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c - url: "https://pub.dev" - source: hosted - version: "2.1.2" draggable_scrollbar: dependency: "direct main" description: @@ -822,54 +814,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_keyboard_visibility_linux: - dependency: transitive - description: - name: flutter_keyboard_visibility_linux - sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_macos: - dependency: transitive - description: - name: flutter_keyboard_visibility_macos - sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 - url: "https://pub.dev" - source: hosted - version: "1.0.0" - flutter_keyboard_visibility_platform_interface: - dependency: transitive - description: - name: flutter_keyboard_visibility_platform_interface - sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_web: - dependency: transitive - description: - name: flutter_keyboard_visibility_web - sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - flutter_keyboard_visibility_windows: - dependency: transitive - description: - name: flutter_keyboard_visibility_windows - sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 - url: "https://pub.dev" - source: hosted - version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -1062,10 +1006,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15 + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" url: "https://pub.dev" source: hosted - version: "12.1.3" + version: "14.2.7" google_fonts: dependency: "direct main" description: @@ -1271,14 +1215,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" - introduction_screen: - dependency: "direct main" - description: - name: introduction_screen - sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5" - url: "https://pub.dev" - source: hosted - version: "3.1.14" io: dependency: "direct dev" description: @@ -1784,14 +1720,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" - retry: - dependency: "direct main" - description: - name: retry - sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" - url: "https://pub.dev" - source: hosted - version: "3.1.2" riverpod: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 77aa3f5b..e4face3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El publish_to: "none" -version: 3.8.0+33 +version: 3.8.1+34 homepage: https://spotube.krtirtho.dev repository: https://github.com/KRTirtho/spotube @@ -23,7 +23,6 @@ dependencies: cached_network_image: ^3.3.1 collection: ^1.15.0 curved_navigation_bar: ^1.0.3 - dbus: ^0.7.8 desktop_webview_window: git: url: https://github.com/KRTirtho/flutter-plugins.git @@ -52,7 +51,6 @@ dependencies: flutter_svg: ^1.1.6 form_validator: ^2.1.1 fuzzywuzzy: ^1.1.6 - go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869 google_fonts: ^6.2.1 hive: ^2.2.3 hive_flutter: ^1.1.0 @@ -60,7 +58,6 @@ dependencies: html: ^0.15.1 image_picker: ^1.1.0 intl: any - introduction_screen: ^3.1.14 json_annotation: ^4.8.1 logger: ^2.0.2 media_kit: ^1.1.10+1 @@ -137,7 +134,7 @@ dependencies: sqlite3_flutter_libs: ^0.5.23 sqlite3: ^2.4.3 encrypt: ^5.0.3 - retry: ^3.1.2 + go_router: ^14.2.7 dev_dependencies: build_runner: ^2.4.9 diff --git a/scripts/windows-setup-creator.iss b/scripts/windows-setup-creator.iss deleted file mode 100644 index 93302234..00000000 --- a/scripts/windows-setup-creator.iss +++ /dev/null @@ -1,59 +0,0 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - -#define MyAppName "Spotube" -#define MyAppVersion "2.0.0" -#define MyAppPublisher "KRTirtho, OSS" -#define MyAppURL "https://github.com/KRTirtho/spotube" -#define MyAppExeName "spotube.exe" - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{80B901C8-D6FE-494E-8AF7-A2BD440E8644} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -;AppVerName={#MyAppName} {#MyAppVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} -DisableProgramGroupPage=yes -; Remove the following line to run in administrative install mode (install for all users.) -PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed=dialog -OutputDir=..\build\installer -OutputBaseFilename=Spotube-windows-x86_64-setup -SetupIconFile=..\windows\runner\resources\app_icon.ico -Compression=lzma -SolidCompression=yes -WizardStyle=modern - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - -[Files] -Source: "..\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\bitsdojo_window_windows_plugin.lib"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\hotkey_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\libwinmedia.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\libwinmedia_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\permission_handler_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\spotube.exp"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\spotube.lib"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "..\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - -[Icons] -Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - -[Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent - diff --git a/windows/packaging/exe/inno_setup.iss b/windows/packaging/exe/inno_setup.iss index dbb8082b..f995d9e9 100644 --- a/windows/packaging/exe/inno_setup.iss +++ b/windows/packaging/exe/inno_setup.iss @@ -20,6 +20,7 @@ Compression=lzma SolidCompression=yes SetupIconFile={{SETUP_ICON_FILE}} WizardStyle=modern +WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp" PrivilegesRequired={{PRIVILEGES_REQUIRED}} ArchitecturesAllowed=x64 ArchitecturesInstallIn64BitMode=x64