Merge pull request #1885 from KRTirtho/dev

chore: release v3.8.1
This commit is contained in:
Kingkor Roy Tirtho 2024-09-15 19:32:29 +06:00 committed by GitHub
commit 87a78549b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 1192 additions and 770 deletions

View File

@ -4,7 +4,7 @@ on:
inputs: inputs:
version: version:
description: Version to publish (x.x.x) description: Version to publish (x.x.x)
default: 3.8.0 default: 3.8.1
required: true required: true
dry_run: dry_run:
description: Dry run description: Dry run
@ -76,12 +76,12 @@ jobs:
commit_message: Updated to v${{ inputs.version }} commit_message: Updated to v${{ inputs.version }}
winget: winget:
runs-on: windows-latest runs-on: ubuntu-latest
if: contains(inputs.jobs, 'winget') if: contains(inputs.jobs, 'winget')
steps: steps:
- name: Release winget package - name: Release winget package
if: ${{ !inputs.dry_run }} if: ${{ !inputs.dry_run }}
uses: vedantmgoyal2009/winget-releaser@v2 uses: vedantmgoyal9/winget-releaser@main
with: with:
version: ${{ inputs.version }} version: ${{ inputs.version }}
release-tag: v${{ inputs.version }} release-tag: v${{ inputs.version }}

View File

@ -101,8 +101,15 @@ jobs:
- name: Unessary hosted tools - name: Unessary hosted tools
if: ${{matrix.platform == 'linux_arm'}} if: ${{matrix.platform == 'linux_arm'}}
run: | uses: jlumbroso/free-disk-space@main
sudo rm -rf /usr/share/dotnet with:
tool-cache: false
swap-storage: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
- name: Build ${{matrix.platform}} binaries - name: Build ${{matrix.platform}} binaries
run: dart cli/cli.dart build ${{matrix.platform}} run: dart cli/cli.dart build ${{matrix.platform}}

View File

@ -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. 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) ## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06)
### Features ### Features

BIN
assets/spotube-logo.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -58,8 +58,6 @@ PODS:
- flutter_inappwebview_ios/Core (0.0.1): - flutter_inappwebview_ios/Core (0.0.1):
- Flutter - Flutter
- OrderedSet (~> 5.0) - OrderedSet (~> 5.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_native_splash (0.0.1): - flutter_native_splash (0.0.1):
- Flutter - Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
@ -124,7 +122,6 @@ DEPENDENCIES:
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`) - flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
- flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`) - flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/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_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`) - flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
@ -173,8 +170,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_discord_rpc/ios" :path: ".symlinks/plugins/flutter_discord_rpc/ios"
flutter_inappwebview_ios: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage: flutter_secure_storage:
@ -220,7 +215,6 @@ SPEC CHECKSUMS:
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882 flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5 flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98 flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98

View File

@ -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<AppPopScope> createState() => _AppPopScopeState();
}
class _AppPopScopeState extends State<AppPopScope> {
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<bool> _handleBackButton() async {
if (_onlyRoute) {
widget.onPopInvoked?.call(widget.canPop);
if (!widget.canPop) {
return true;
}
}
return false;
}
}

View File

@ -41,29 +41,33 @@ class PanelController extends ChangeNotifier {
bool get isAttached => _panelState != null; bool get isAttached => _panelState != null;
/// Closes the sliding panel to its collapsed state (i.e. to the minHeight) /// Closes the sliding panel to its collapsed state (i.e. to the minHeight)
Future<void> close() { Future<void> close() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._close(); await _panelState!._close();
notifyListeners();
} }
/// Opens the sliding panel fully /// Opens the sliding panel fully
/// (i.e. to the maxHeight) /// (i.e. to the maxHeight)
Future<void> open() { Future<void> open() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._open(); await _panelState!._open();
notifyListeners();
} }
/// Hides the sliding panel (i.e. is invisible) /// Hides the sliding panel (i.e. is invisible)
Future<void> hide() { Future<void> hide() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._hide(); await _panelState!._hide();
notifyListeners();
} }
/// Shows the sliding panel in its collapsed state /// Shows the sliding panel in its collapsed state
/// (i.e. "un-hide" the sliding panel) /// (i.e. "un-hide" the sliding panel)
Future<void> show() { Future<void> show() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel"); assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._show(); await _panelState!._show();
notifyListeners();
} }
/// Animates the panel position to the value. /// Animates the panel position to the value.

View File

@ -58,7 +58,7 @@ class PlaybuttonCard extends HookWidget {
others: 15, others: 15,
); );
var unescapeHtml = description?.unescapeHtml(); final unescapeHtml = description?.unescapeHtml().cleanHtml();
return Container( return Container(
constraints: BoxConstraints(maxWidth: size), constraints: BoxConstraints(maxWidth: size),
margin: margin, margin: margin,

View File

@ -105,7 +105,9 @@ class TrackOptions extends HookConsumerWidget {
final pages = final pages =
await spotify.search.get(query, types: [SearchType.playlist]).first(); await spotify.search.get(query, types: [SearchType.playlist]).first();
final radios = pages.map((e) => e.items).toList().cast<PlaylistSimple>(); final radios = pages
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
.toList();
final artists = track.artists!.map((e) => e.name); final artists = track.artists!.map((e) => e.name);

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.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/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class TrackTile extends HookConsumerWidget { class TrackTile extends HookConsumerWidget {
@ -276,6 +278,7 @@ class TrackTile extends HookConsumerWidget {
userPlaylist: userPlaylist, userPlaylist: userPlaylist,
showMenuCbRef: showOptionCbRef, showMenuCbRef: showOptionCbRef,
), ),
if (kIsDesktop) const Gap(10),
], ],
), ),
), ),

View File

@ -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/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_props.dart';
import 'package:spotube/components/tracks_view/track_view_provider.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/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart'; import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
@ -65,6 +66,56 @@ class TrackViewBodySection extends HookConsumerWidget {
final isActive = playlist.collections.contains(props.collectionId); 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( return SliverMainAxisGroup(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@ -130,58 +181,7 @@ class TrackViewBodySection extends HookConsumerWidget {
trackViewState.selectTrack(track.id!); trackViewState.selectTrack(track.id!);
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
}, },
onTap: () async { onTap: () => onTapTrackTile(track, index),
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]);
}
}
}
},
); );
}, },
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart'; import 'package:spotube/components/expandable_search/expandable_search.dart';
import 'package:spotube/components/sort_tracks_dropdown.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/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/utils/platform.dart';
class TrackViewBodyHeaders extends HookConsumerWidget { class TrackViewBodyHeaders extends HookConsumerWidget {
final ValueNotifier<bool> isFiltering; final ValueNotifier<bool> isFiltering;
@ -94,6 +96,7 @@ class TrackViewBodyHeaders extends HookConsumerWidget {
}, },
), ),
const TrackViewBodyOptions(), const TrackViewBodyOptions(),
if (kIsDesktop) const Gap(10),
], ],
); );
}, },

View File

@ -128,7 +128,9 @@ class TrackViewFlexHeader extends HookConsumerWidget {
if (props.description != null && if (props.description != null &&
props.description!.isNotEmpty) props.description!.isNotEmpty)
Text( Text(
props.description!.unescapeHtml(), props.description!
.unescapeHtml()
.cleanHtml(),
style: style:
defaultTextStyle.style.copyWith( defaultTextStyle.style.copyWith(
color: palette.bodyTextColor, color: palette.bodyTextColor,

19
lib/extensions/list.dart Normal file
View File

@ -0,0 +1,19 @@
extension UniqueItemExtension<T> on List<T> {
List<T> unique(bool Function(T a, T b) equals) {
final copy = <T>[];
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;
}
}

View File

@ -1,12 +1,15 @@
import 'package:html_unescape/html_unescape.dart'; import 'package:html_unescape/html_unescape.dart';
import 'package:html/parser.dart';
final htmlEscape = HtmlUnescape(); final htmlEscape = HtmlUnescape();
extension UnescapeHtml on String { extension UnescapeHtml on String {
String cleanHtml() => parse("<p>$this</p>").documentElement!.text;
String unescapeHtml() => htmlEscape.convert(this); String unescapeHtml() => htmlEscape.convert(this);
} }
extension NullableUnescapeHtml on String? { extension NullableUnescapeHtml on String? {
String? cleanHtml() => this?.cleanHtml();
String? unescapeHtml() => this?.unescapeHtml(); String? unescapeHtml() => this?.unescapeHtml();
} }

View File

@ -7,6 +7,7 @@ import 'package:spotube/collections/routes.dart';
import 'package:spotube/provider/spotify_provider.dart'; import 'package:spotube/provider/spotify_provider.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
final appLinks = AppLinks(); final appLinks = AppLinks();
@ -61,6 +62,7 @@ void useDeepLinking(WidgetRef ref) {
} }
final subscription = linkStream.listen((uri) async { final subscription = linkStream.listen((uri) async {
try {
final startSegment = uri.split(":").take(2).join(":"); final startSegment = uri.split(":").take(2).join(":");
final endSegment = uri.split(":").last; final endSegment = uri.split(":").last;
@ -86,6 +88,9 @@ void useDeepLinking(WidgetRef ref) {
default: default:
break; break;
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
return () { return () {

View File

@ -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;
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "حصلت على حبك", "summary_got_your_love": "حصلت على حبك",
"summary_playlists": "قوائم التشغيل", "summary_playlists": "قوائم التشغيل",
"summary_were_on_repeat": "كانت على التكرار", "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": "المنصة غير مدعومة"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "আপনার ভালোবাসা পেয়েছে", "summary_got_your_love": "আপনার ভালোবাসা পেয়েছে",
"summary_playlists": "প্লেলিস্ট", "summary_playlists": "প্লেলিস্ট",
"summary_were_on_repeat": "পুনরাবৃত্তিতে ছিল", "summary_were_on_repeat": "পুনরাবৃত্তিতে ছিল",
"total_money": "মোট {money}" "total_money": "মোট {money}",
"webview_not_found": "ওয়েবভিউ পাওয়া যায়নি",
"webview_not_found_description": "আপনার ডিভাইসে কোনো ওয়েবভিউ রানটাইম ইনস্টল করা নেই।\nযদি ইনস্টল থাকে, তা নিশ্চিত করুন যে এটি environment PATH এ রয়েছে\n\nইনস্টল করার পর, অ্যাপটি পুনরায় চালু করুন",
"unsupported_platform": "সমর্থিত প্ল্যাটফর্ম নয়"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "ha aconseguit el teu amor", "summary_got_your_love": "ha aconseguit el teu amor",
"summary_playlists": "llistes de reproducció", "summary_playlists": "llistes de reproducció",
"summary_were_on_repeat": "estaven en repetició", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Získal vaši lásku", "summary_got_your_love": "Získal vaši lásku",
"summary_playlists": "playlisty", "summary_playlists": "playlisty",
"summary_were_on_repeat": "Byly na opakování", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Hat Ihre Liebe gewonnen", "summary_got_your_love": "Hat Ihre Liebe gewonnen",
"summary_playlists": "Wiedergabelisten", "summary_playlists": "Wiedergabelisten",
"summary_were_on_repeat": "Wurden wiederholt", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Got your love", "summary_got_your_love": "Got your love",
"summary_playlists": "playlists", "summary_playlists": "playlists",
"summary_were_on_repeat": "Were on repeat", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Obtuvo tu amor", "summary_got_your_love": "Obtuvo tu amor",
"summary_playlists": "listas de reproducción", "summary_playlists": "listas de reproducción",
"summary_were_on_repeat": "Estaban en repetició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"
} }

View File

@ -367,7 +367,7 @@
"count_plays": "{count} erreprodukzio", "count_plays": "{count} erreprodukzio",
"streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)", "streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)",
"minutes_listened": "Entzundako minutuak", "minutes_listened": "Entzundako minutuak",
"streamed_songs": "Stream-eatutako kantak", "streamed_songs": "Streaming-ez entzundako kantak",
"count_streams": "{count} stream", "count_streams": "{count} stream",
"owned_by_you": "Zure jabetzakoa", "owned_by_you": "Zure jabetzakoa",
"copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua", "copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua",
@ -376,13 +376,16 @@
"summary_minutes": "minutu", "summary_minutes": "minutu",
"summary_listened_to_music": "Musika entzuten", "summary_listened_to_music": "Musika entzuten",
"summary_songs": "kanta", "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_owed_to_artists": "Hilabete honetan\nartistei zor zaiena",
"summary_artists": "artisten", "summary_artists": "artisten",
"summary_music_reached_you": "Musika ailegatu zaizu", "summary_music_reached_you": "Musika ailegatu zaizu",
"summary_full_albums": "album osok", "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_playlists": "zerrenda",
"summary_were_on_repeat": "Dituzu errepikatze moduan", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "عشق شما را به دست آورد", "summary_got_your_love": "عشق شما را به دست آورد",
"summary_playlists": "لیست‌های پخش", "summary_playlists": "لیست‌های پخش",
"summary_were_on_repeat": "در تکرار بودند", "summary_were_on_repeat": "در تکرار بودند",
"total_money": "مجموع {money}" "total_money": "مجموع {money}",
"webview_not_found": "وب‌ویو پیدا نشد",
"webview_not_found_description": "هیچ اجرای وب‌ویو روی دستگاه شما نصب نشده است.\nدر صورت نصب، مطمئن شوید که در environment PATH قرار دارد\n\nپس از نصب، برنامه را مجدداً راه‌اندازی کنید",
"unsupported_platform": "پلتفرم پشتیبانی نمی‌شود"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Sai rakkautesi", "summary_got_your_love": "Sai rakkautesi",
"summary_playlists": "soittolistat", "summary_playlists": "soittolistat",
"summary_were_on_repeat": "Olivat toistossa", "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"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "A obtenu votre amour", "summary_got_your_love": "A obtenu votre amour",
"summary_playlists": "playlists", "summary_playlists": "playlists",
"summary_were_on_repeat": "Était en répétition", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} स्ट्रिम", "count_streams": "{count} स्ट्रिम",
"owned_by_you": "तपाईंले स्वामित्व गरेको", "owned_by_you": "तपाईंले स्वामित्व गरेको",
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", "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": "असमर्थित प्लेटफार्म"
} }

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Mendapatkan cinta Anda", "summary_got_your_love": "Mendapatkan cinta Anda",
"summary_playlists": "daftar putar", "summary_playlists": "daftar putar",
"summary_were_on_repeat": "Sedang diulang", "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"
} }

View File

@ -385,5 +385,8 @@
"summary_got_your_love": "Ha ricevuto il tuo amore", "summary_got_your_love": "Ha ricevuto il tuo amore",
"summary_playlists": "playlist", "summary_playlists": "playlist",
"summary_were_on_repeat": "Erano in ripetizione", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} 回のストリーム", "count_streams": "{count} 回のストリーム",
"owned_by_you": "あなたが所有", "owned_by_you": "あなたが所有",
"copied_shareurl_to_clipboard": "{shareUrl} をクリップボードにコピーしました", "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": "サポートされていないプラットフォーム"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} სტრიმი", "count_streams": "{count} სტრიმი",
"owned_by_you": "შენ მიერ საკუთრებული", "owned_by_you": "შენ მიერ საკუთრებული",
"copied_shareurl_to_clipboard": "{shareUrl} აიღო კლიპბორდზე", "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": "მოუხერხებელი პლატფორმა"
} }

View File

@ -385,5 +385,8 @@
"count_streams": "{count} 스트림", "count_streams": "{count} 스트림",
"owned_by_you": "당신이 소유", "owned_by_you": "당신이 소유",
"copied_shareurl_to_clipboard": "{shareUrl}를 클립보드에 복사했습니다", "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": "지원되지 않는 플랫폼"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} स्ट्रिम", "count_streams": "{count} स्ट्रिम",
"owned_by_you": "तपाईंले स्वामित्व गरेको", "owned_by_you": "तपाईंले स्वामित्व गरेको",
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो", "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": "असमर्थित प्लेटफार्म"
} }

View File

@ -385,5 +385,8 @@
"count_streams": "{count} streams", "count_streams": "{count} streams",
"owned_by_you": "Bezit door jou", "owned_by_you": "Bezit door jou",
"copied_shareurl_to_clipboard": "{shareUrl} gekopieerd naar klembord", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} strumieni", "count_streams": "{count} strumieni",
"owned_by_you": "Własność Twoja", "owned_by_you": "Własność Twoja",
"copied_shareurl_to_clipboard": "{shareUrl} skopiowano do schowka", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} streams", "count_streams": "{count} streams",
"owned_by_you": "De sua propriedade", "owned_by_you": "De sua propriedade",
"copied_shareurl_to_clipboard": "{shareUrl} copiado para a área de transferência", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} стримов", "count_streams": "{count} стримов",
"owned_by_you": "Ваша собственность", "owned_by_you": "Ваша собственность",
"copied_shareurl_to_clipboard": "{shareUrl} скопировано в буфер обмена", "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": "Платформа не поддерживается"
} }

View File

@ -385,5 +385,8 @@
"count_streams": "{count} สตรีม", "count_streams": "{count} สตรีม",
"owned_by_you": "เป็นเจ้าของโดยคุณ", "owned_by_you": "เป็นเจ้าของโดยคุณ",
"copied_shareurl_to_clipboard": "{shareUrl} คัดลอกไปที่คลิปบอร์ดแล้ว", "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": "แพลตฟอร์มไม่รองรับ"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} yayın", "count_streams": "{count} yayın",
"owned_by_you": "Sahip olduğunuz", "owned_by_you": "Sahip olduğunuz",
"copied_shareurl_to_clipboard": "{shareUrl} panoya kopyalandı", "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"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} стримів", "count_streams": "{count} стримів",
"owned_by_you": "Ваша власність", "owned_by_you": "Ваша власність",
"copied_shareurl_to_clipboard": "{shareUrl} скопійовано в буфер обміну", "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": "Непідтримувана платформа"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} lượt phát", "count_streams": "{count} lượt phát",
"owned_by_you": "Thuộc sở hữu của bạn", "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", "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ợ"
} }

View File

@ -384,5 +384,8 @@
"count_streams": "{count} 次流媒体", "count_streams": "{count} 次流媒体",
"owned_by_you": "由您拥有", "owned_by_you": "由您拥有",
"copied_shareurl_to_clipboard": "{shareUrl} 已复制到剪贴板", "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": "不支持的平台"
} }

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.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_disable_battery_optimizations.dart';
import 'package:spotube/hooks/configurators/use_fix_window_stretching.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_get_storage_perms.dart';
import 'package:spotube/hooks/configurators/use_has_touch.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player_streams.dart'; import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
@ -92,7 +94,7 @@ Future<void> main(List<String> rawArgs) async {
await FlutterDiscordRPC.initialize(Env.discordAppId); await FlutterDiscordRPC.initialize(Env.discordAppId);
} }
if(kIsWindows){ if (kIsWindows) {
await SMTCWindows.initialize(); await SMTCWindows.initialize();
} }
@ -142,6 +144,7 @@ class Spotube extends HookConsumerWidget {
final paletteColor = final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
final hasTouchSupport = useHasTouch();
ref.listen(audioPlayerStreamListenersProvider, (_, __) {}); ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {}); ref.listen(bonsoirProvider, (_, __) {});
@ -191,8 +194,22 @@ class Spotube extends HookConsumerWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Spotube', title: 'Spotube',
builder: (context, child) { builder: (context, child) {
if (kIsDesktop && !kIsMacOS) return DragToResizeArea(child: child!); child = ScrollConfiguration(
return child!; 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, themeMode: themeMode,
theme: lightTheme, theme: lightTheme,

View File

@ -1,6 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
int useSyncedLyrics( int useSyncedLyrics(
WidgetRef ref, WidgetRef ref,
@ -13,9 +14,13 @@ int useSyncedLyrics(
useEffect(() { useEffect(() {
return stream.listen((pos) { return stream.listen((pos) {
try {
if (lyricsMap.containsKey(pos.inSeconds + delay)) { if (lyricsMap.containsKey(pos.inSeconds + delay)) {
currentTime.value = pos.inSeconds + delay; currentTime.value = pos.inSeconds + delay;
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}).cancel; }).cancel;
}, [lyricsMap, delay]); }, [lyricsMap, delay]);

View File

@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.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_actions.dart';
import 'package:spotube/modules/player/player_controls.dart'; import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/player_queue.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; final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
// ignore: deprecated_member_use return AppPopScope(
return WillPopScope( canPop: context.canPop(),
onWillPop: () async { onPopInvoked: (didPop) async {
await panelController.close(); await panelController.close();
return false;
}, },
child: IconTheme( child: IconTheme(
data: theme.iconTheme.copyWith(color: bodyTextColor), data: theme.iconTheme.copyWith(color: bodyTextColor),

View File

@ -170,10 +170,9 @@ class PlayerControls extends HookConsumerWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
StreamBuilder<bool>( Consumer(builder: (context, ref, _) {
stream: audioPlayer.shuffledStream, final shuffled = ref
builder: (context, snapshot) { .watch(audioPlayerProvider.select((s) => s.shuffled));
final shuffled = snapshot.data ?? false;
return IconButton( return IconButton(
tooltip: shuffled tooltip: shuffled
? context.l10n.unshuffle_playlist ? context.l10n.unshuffle_playlist

View File

@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart'; import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:flutter/material.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/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -30,7 +29,6 @@ class BottomPlayer extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
final playlist = ref.watch(audioPlayerProvider); final playlist = ref.watch(audioPlayerProvider);
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -89,7 +87,6 @@ class BottomPlayer extends HookConsumerWidget {
children: [ children: [
PlayerActions( PlayerActions(
extraActions: [ extraActions: [
if (auth.asData?.value != null)
IconButton( IconButton(
tooltip: context.l10n.mini_player, tooltip: context.l10n.mini_player,
icon: const Icon(SpotubeIcons.miniPlayer), icon: const Icon(SpotubeIcons.miniPlayer),

View File

@ -6,7 +6,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/home/home.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:spotube/services/kv_store/kv_store.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -16,6 +16,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context); final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final onLogin = useLoginCallback(ref);
return Center( return Center(
child: Column( child: Column(
@ -121,9 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
), ),
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) { await onLogin();
context.pushNamed(WebViewLogin.name);
}
}, },
), ),
], ],

View File

@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.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/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/themed_button_tab_bar.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/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_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/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.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) { if (isModal) {
return DefaultTabController( return DefaultTabController(
length: 2, length: 2,

View File

@ -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_controls.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/root/sidebar.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/extensions/context.dart';
import 'package:spotube/hooks/utils/use_force_update.dart'; import 'package:spotube/hooks/utils/use_force_update.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart'; import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_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/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -46,14 +43,7 @@ class MiniLyricsPage extends HookConsumerWidget {
return null; return null;
}, []); }, []);
final auth = ref.watch(authenticationProvider);
if (auth.asData?.value == null) {
return const Scaffold(
appBar: PageWindowTitleBar(),
body: AnonymousFallback(),
);
}
return MouseRegion( return MouseRegion(
onEnter: !hoverMode.value onEnter: !hoverMode.value

View File

@ -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/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:stroke_text/stroke_text.dart'; import 'package:stroke_text/stroke_text.dart';
@ -80,12 +81,16 @@ class SyncedLyrics extends HookConsumerWidget {
StreamSubscription? subscription; StreamSubscription? subscription;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
subscription = audioPlayer.positionStream.listen((event) { subscription = audioPlayer.positionStream.listen((event) {
if (event > Duration.zero) return; try {
if (event > Duration.zero || !controller.hasClients) return;
controller.animateTo( controller.animateTo(
0, 0,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
}); });

View File

@ -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<void> 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]);
}

View File

@ -27,7 +27,7 @@ class WebViewLogin extends HookConsumerWidget {
child: InAppWebView( child: InAppWebView(
initialSettings: InAppWebViewSettings( initialSettings: InAppWebViewSettings(
userAgent: 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( initialUrlRequest: URLRequest(
url: WebUri("https://accounts.spotify.com/"), url: WebUri("https://accounts.spotify.com/"),

View File

@ -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',
}),
),
],
);
}
}

View File

@ -5,7 +5,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/bottom_player.dart';
@ -30,10 +32,11 @@ class RootApp extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final showingDialogCompleter = useRef(Completer()..complete()); final showingDialogCompleter = useRef(Completer()..complete());
final downloader = ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider);
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
final theme = Theme.of(context);
final connectRoutes = ref.watch(serverConnectRoutesProvider); final connectRoutes = ref.watch(serverConnectRoutesProvider);
useEffect(() { useEffect(() {
@ -164,17 +167,11 @@ class RootApp extends HookConsumerWidget {
return null; return null;
}, [backgroundColor]); }, [backgroundColor]);
// ignore: deprecated_member_use final navTileNames = useMemoized(() {
return WillPopScope( return getSidebarTileList(context.l10n).map((s) => s.name).toList();
onWillPop: () async { }, []);
final routerState = GoRouterState.of(context);
if (routerState.matchedLocation != "/") { final scaffold = Scaffold(
context.goNamed(HomePage.name);
return false;
}
return true;
},
child: Scaffold(
body: Sidebar(child: child), body: Sidebar(child: child),
extendBody: true, extendBody: true,
drawerScrimColor: Colors.transparent, drawerScrimColor: Colors.transparent,
@ -212,7 +209,27 @@ class RootApp extends HookConsumerWidget {
SpotubeNavigationBar(), 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,
); );
} }
} }

View File

@ -7,6 +7,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/logs/logs_provider.dart'; import 'package:spotube/provider/logs/logs_provider.dart';
import 'package:spotube/services/logger/logger.dart';
class LogsPage extends HookConsumerWidget { class LogsPage extends HookConsumerWidget {
static const name = "logs"; 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( body: SafeArea(

View File

@ -1,24 +1,18 @@
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart'; 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:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/collections/spotube_icons.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.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/profile/profile.dart';
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class SettingsAccountSection extends HookConsumerWidget { class SettingsAccountSection extends HookConsumerWidget {
@ -30,7 +24,6 @@ class SettingsAccountSection extends HookConsumerWidget {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final authNotifier = ref.watch(authenticationProvider.notifier);
final scrobbler = ref.watch(scrobblerProvider); final scrobbler = ref.watch(scrobblerProvider);
final me = ref.watch(meProvider); final me = ref.watch(meProvider);
final meData = me.asData?.value; final meData = me.asData?.value;
@ -40,51 +33,7 @@ class SettingsAccountSection extends HookConsumerWidget {
foregroundColor: Colors.white, foregroundColor: Colors.white,
); );
void onLogin() async { final onLogin = useLoginCallback(ref);
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;
});
}
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.account, heading: context.l10n.account,

View File

@ -49,8 +49,8 @@ class StatsMinutesPage extends HookConsumerWidget {
return StatsTrackItem( return StatsTrackItem(
track: track.track, track: track.track,
info: Text( info: Text(
context.l10n context.l10n.count_mins(compactNumberFormatter
.count_plays(compactNumberFormatter.format(track.count)), .format(track.count * track.track.duration!.inMinutes)),
), ),
); );
}, },

View File

@ -49,8 +49,8 @@ class StatsStreamsPage extends HookConsumerWidget {
return StatsTrackItem( return StatsTrackItem(
track: track.track, track: track.track,
info: Text( info: Text(
context.l10n.count_mins(compactNumberFormatter context.l10n
.format(track.count * track.track.duration!.inMinutes)), .count_plays(compactNumberFormatter.format(track.count)),
), ),
); );
}, },

View File

@ -14,6 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/track_tile/track_options.dart'; import 'package:spotube/components/track_tile/track_options.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.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/audio_player/audio_player.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -167,7 +168,8 @@ class TrackPage extends HookConsumerWidget {
children: [ children: [
const Gap(5), const Gap(5),
if (!isActive && if (!isActive &&
!playlist.tracks.contains(track)) !playlist.tracks
.containsBy(track, (t) => t.id))
OutlinedButton.icon( OutlinedButton.icon(
icon: const Icon(SpotubeIcons.queueAdd), icon: const Icon(SpotubeIcons.queueAdd),
label: Text(context.l10n.queue), label: Text(context.l10n.queue),
@ -177,7 +179,8 @@ class TrackPage extends HookConsumerWidget {
), ),
const Gap(5), const Gap(5),
if (!isActive && if (!isActive &&
!playlist.tracks.contains(track)) !playlist.tracks
.containsBy(track, (t) => t.id))
IconButton.outlined( IconButton.outlined(
icon: icon:
const Icon(SpotubeIcons.lightning), const Icon(SpotubeIcons.lightning),

View File

@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotify/spotify.dart' hide Playlist; import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/extensions/list.dart';
import 'package:spotube/extensions/track.dart'; import 'package:spotube/extensions/track.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.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/discord_provider.dart';
import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/server/sourced_track.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
class AudioPlayerNotifier extends Notifier<AudioPlayerState> { class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier); BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
@ -141,6 +143,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
build() { build() {
final subscriptions = [ final subscriptions = [
audioPlayer.playingStream.listen((playing) async { audioPlayer.playingStream.listen((playing) async {
try {
state = state.copyWith(playing: playing); state = state.copyWith(playing: playing);
await _updatePlayerState( await _updatePlayerState(
@ -148,8 +151,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
playing: Value(playing), playing: Value(playing),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.loopModeStream.listen((loopMode) async { audioPlayer.loopModeStream.listen((loopMode) async {
try {
state = state.copyWith(loopMode: loopMode); state = state.copyWith(loopMode: loopMode);
await _updatePlayerState( await _updatePlayerState(
@ -157,8 +164,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
loopMode: Value(loopMode), loopMode: Value(loopMode),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.shuffledStream.listen((shuffled) async { audioPlayer.shuffledStream.listen((shuffled) async {
try {
state = state.copyWith(shuffled: shuffled); state = state.copyWith(shuffled: shuffled);
await _updatePlayerState( await _updatePlayerState(
@ -166,11 +177,18 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
shuffled: Value(shuffled), shuffled: Value(shuffled),
), ),
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.playlistStream.listen((playlist) async { audioPlayer.playlistStream.listen((playlist) async {
try {
state = state.copyWith(playlist: playlist); 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<AudioPlayerState> {
for (int i = 0; i < tracks.length; i++) { for (int i = 0; i < tracks.length; i++) {
final track = tracks.elementAt(i); final track = tracks.elementAt(i);
if (state.tracks.any((element) => _compareTracks(element, track))) {
continue;
}
await audioPlayer.addTrackAt( await audioPlayer.addTrackAt(
SpotubeMedia(track), SpotubeMedia(track),
max(state.playlist.index, 0) + i + 1, max(state.playlist.index, 0) + i + 1,
@ -248,6 +270,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
Future<void> addTrack(Track track) async { Future<void> addTrack(Track track) async {
if (_blacklist.contains(track)) return; if (_blacklist.contains(track)) return;
if (state.tracks.any((element) => _compareTracks(element, track))) return;
await audioPlayer.addTrack(SpotubeMedia(track)); await audioPlayer.addTrack(SpotubeMedia(track));
} }
@ -272,13 +295,23 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
} }
} }
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<void> load( Future<void> load(
List<Track> tracks, { List<Track> tracks, {
int initialIndex = 0, int initialIndex = 0,
bool autoPlay = false, bool autoPlay = false,
}) async { }) async {
final medias = final medias = (_blacklist.filter(tracks).toList() as List<Track>)
(_blacklist.filter(tracks).toList() as List<Track>).asMediaList(); .asMediaList()
.unique((a, b) => _compareTracks(a.track, b.track));
// Giving the initial track a boost so MediaKit won't skip // Giving the initial track a boost so MediaKit won't skip
// because of timeout // because of timeout

View File

@ -73,14 +73,19 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPlaylist() { StreamSubscription subscribeToPlaylist() {
return audioPlayer.playlistStream.listen((mpvPlaylist) { return audioPlayer.playlistStream.listen((mpvPlaylist) {
try {
notificationService.addTrack(audioPlayerState.activeTrack!); notificationService.addTrack(audioPlayerState.activeTrack!);
discord.updatePresence(audioPlayerState.activeTrack!); discord.updatePresence(audioPlayerState.activeTrack!);
updatePalette(); updatePalette();
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }
StreamSubscription subscribeToSkipSponsor() { StreamSubscription subscribeToSkipSponsor() {
return audioPlayer.positionStream.listen((position) async { return audioPlayer.positionStream.listen((position) async {
try {
final currentSegments = await ref.read(segmentProvider.future); final currentSegments = await ref.read(segmentProvider.future);
if (currentSegments?.segments.isNotEmpty != true || if (currentSegments?.segments.isNotEmpty != true ||
@ -93,6 +98,9 @@ class AudioPlayerStreamListeners {
await audioPlayer.seek(Duration(seconds: segment.end + 1)); await audioPlayer.seek(Duration(seconds: segment.end + 1));
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }
@ -122,13 +130,15 @@ class AudioPlayerStreamListeners {
StreamSubscription subscribeToPosition() { StreamSubscription subscribeToPosition() {
String lastTrack = ""; // used to prevent multiple calls to the same track String lastTrack = ""; // used to prevent multiple calls to the same track
return audioPlayer.positionStream.listen((event) async { return audioPlayer.positionStream.listen((event) async {
try {
if (event < const Duration(seconds: 3) || if (event < const Duration(seconds: 3) ||
audioPlayerState.playlist.index == -1 || audioPlayerState.playlist.index == -1 ||
audioPlayerState.playlist.index == audioPlayerState.playlist.index ==
audioPlayerState.tracks.length - 1) { audioPlayerState.tracks.length - 1) {
return; return;
} }
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias final nextTrack = SpotubeMedia.fromMedia(audioPlayerState
.playlist.medias
.elementAt(audioPlayerState.playlist.index + 1)); .elementAt(audioPlayerState.playlist.index + 1));
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) { if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
@ -140,6 +150,9 @@ class AudioPlayerStreamListeners {
} finally { } finally {
lastTrack = nextTrack.track.id!; lastTrack = nextTrack.track.id!;
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }

View File

@ -1,6 +1,7 @@
import 'package:bonsoir/bonsoir.dart'; import 'package:bonsoir/bonsoir.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/device_info/device_info.dart'; import 'package:spotube/services/device_info/device_info.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectClientsState { class ConnectClientsState {
final List<BonsoirService> services; final List<BonsoirService> services;
@ -37,6 +38,7 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
final subscription = discovery.eventStream?.listen((event) { final subscription = discovery.eventStream?.listen((event) {
// ignore device itself // ignore device itself
try {
if (event.service?.attributes["deviceId"] == deviceId) { if (event.service?.attributes["deviceId"] == deviceId) {
return; return;
} }
@ -65,7 +67,8 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
.toList(), .toList(),
discovery: state.value!.discovery, discovery: state.value!.discovery,
resolvedService: state.value?.resolvedService != null && resolvedService: state.value?.resolvedService != null &&
event.service?.name == state.value?.resolvedService?.name event.service?.name ==
state.value?.resolvedService?.name
? null ? null
: state.value!.resolvedService, : state.value!.resolvedService,
), ),
@ -74,6 +77,9 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
default: default:
break; break;
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
ref.onDispose(() { ref.onDispose(() {

View File

@ -7,11 +7,14 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class DiscordNotifier extends AsyncNotifier<void> { class DiscordNotifier extends AsyncNotifier<void> {
@override @override
FutureOr<void> build() async { FutureOr<void> build() async {
if (!kIsDesktop) return;
final enabled = ref.watch( final enabled = ref.watch(
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop)); userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
@ -19,18 +22,27 @@ class DiscordNotifier extends AsyncNotifier<void> {
final subscriptions = [ final subscriptions = [
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async { FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
try {
final playback = ref.read(audioPlayerProvider); final playback = ref.read(audioPlayerProvider);
if (connected && playback.activeTrack != null) { if (connected && playback.activeTrack != null) {
await updatePresence(playback.activeTrack!); await updatePresence(playback.activeTrack!);
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}), }),
audioPlayer.playerStateStream.listen((state) async { audioPlayer.playerStateStream.listen((state) async {
try {
final playback = ref.read(audioPlayerProvider); final playback = ref.read(audioPlayerProvider);
if (playback.activeTrack == null) return; 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 { audioPlayer.positionStream.listen((position) async {
try {
final playback = ref.read(audioPlayerProvider); final playback = ref.read(audioPlayerProvider);
if (playback.activeTrack != null) { if (playback.activeTrack != null) {
final diff = position.inMilliseconds - lastPosition.inMilliseconds; final diff = position.inMilliseconds - lastPosition.inMilliseconds;
@ -39,6 +51,9 @@ class DiscordNotifier extends AsyncNotifier<void> {
} }
} }
lastPosition = position; lastPosition = position;
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}) })
]; ];
@ -46,6 +61,7 @@ class DiscordNotifier extends AsyncNotifier<void> {
for (final subscription in subscriptions) { for (final subscription in subscriptions) {
subscription.cancel(); subscription.cancel();
} }
await clear();
await close(); await close();
await FlutterDiscordRPC.instance.dispose(); await FlutterDiscordRPC.instance.dispose();
}); });
@ -53,12 +69,14 @@ class DiscordNotifier extends AsyncNotifier<void> {
if (!enabled && FlutterDiscordRPC.instance.isConnected) { if (!enabled && FlutterDiscordRPC.instance.isConnected) {
await clear(); await clear();
await close(); await close();
} else { } else if (enabled) {
await FlutterDiscordRPC.instance.connect(autoRetry: true); await FlutterDiscordRPC.instance.connect(autoRetry: true);
} }
} }
Future<void> updatePresence(Track track) async { Future<void> updatePresence(Track track) async {
if (!kIsDesktop) return;
if (FlutterDiscordRPC.instance.isConnected == false) return;
final artistNames = track.artists?.asString(); final artistNames = track.artists?.asString();
final isPlaying = audioPlayer.isPlaying; final isPlaying = audioPlayer.isPlaying;
final position = audioPlayer.position; final position = audioPlayer.position;
@ -92,10 +110,12 @@ class DiscordNotifier extends AsyncNotifier<void> {
} }
Future<void> clear() async { Future<void> clear() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.clearActivity(); await FlutterDiscordRPC.instance.clearActivity();
} }
Future<void> close() async { Future<void> close() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.disconnect(); await FlutterDiscordRPC.instance.disconnect();
} }
} }

View File

@ -23,6 +23,7 @@ class DownloadManagerProvider extends ChangeNotifier {
$backHistory = <Track>{}, $backHistory = <Track>{},
dl = DownloadManager() { dl = DownloadManager() {
dl.statusStream.listen((event) async { dl.statusStream.listen((event) async {
try {
final (:request, :status) = event; final (:request, :status) = event;
final track = $history.firstWhereOrNull( final track = $history.firstWhereOrNull(
@ -85,6 +86,9 @@ class DownloadManagerProvider extends ChangeNotifier {
file: file.path, file: file.path,
metadata: metadata, metadata: metadata,
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }

View File

@ -90,12 +90,18 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final albumsQuery = createAlbumsQuery(limit: limit, offset: offset); 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 @override
build(arg) async { 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) { final subscription = createAlbumsQuery().watch().listen((event) {
if (state.asData == null) return; if (state.asData == null) return;
@ -111,9 +117,9 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
return HistoryTopAlbumsState( return HistoryTopAlbumsState(
items: albums, items: albums,
offset: albums.length, offset: nextOffset,
limit: 20, limit: 20,
hasMore: true, hasMore: hasMore,
); );
} }

View File

@ -55,12 +55,18 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset); 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 @override
build(arg) async { 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) { final subscription = createPlaylistsQuery().watch().listen((event) {
if (state.asData == null) return; if (state.asData == null) return;
@ -76,9 +82,9 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
return HistoryTopPlaylistsState( return HistoryTopPlaylistsState(
items: playlists, items: playlists,
offset: playlists.length, offset: nextOffset,
limit: 20, limit: 20,
hasMore: true, hasMore: hasMore,
); );
} }

View File

@ -89,12 +89,18 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracksQuery = createTracksQuery()..limit(limit, offset: offset); 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 @override
build(arg) async { 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) { final subscription = createTracksQuery().watch().listen((event) {
if (state.asData == null) return; if (state.asData == null) return;
@ -110,9 +116,9 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
return HistoryTopTracksState( return HistoryTopTracksState(
items: tracks, items: tracks,
offset: tracks.length, offset: nextOffset,
limit: 20, limit: 20,
hasMore: true, hasMore: hasMore,
); );
} }

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -72,14 +73,11 @@ final localTracksProvider =
} }
} }
final List<Map<dynamic, dynamic>> filesWithMetadata = []; final List<Map<dynamic, dynamic>> filesWithMetadata = await Future.wait(
entities.map((file) async {
for (final file in entities) {
try { try {
final metadata = await MetadataGod.readMetadata(file: file.path); final metadata = await MetadataGod.readMetadata(file: file.path);
await Future.delayed(const Duration(milliseconds: 50));
final imageFile = File(join( final imageFile = File(join(
(await getTemporaryDirectory()).path, (await getTemporaryDirectory()).path,
"spotube", "spotube",
@ -94,17 +92,16 @@ final localTracksProvider =
); );
} }
filesWithMetadata.add( return {"metadata": metadata, "file": file, "art": imageFile.path};
{"metadata": metadata, "file": file, "art": imageFile.path},
);
} catch (e, stack) { } catch (e, stack) {
if (e case FrbException() || TimeoutException()) { if (e case FrbException() || TimeoutException()) {
filesWithMetadata.add({"file": file}); return {"file": file};
} }
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
continue; return null;
}
} }
}),
).then((value) => value.whereNotNull().toList());
final tracksFromMetadata = filesWithMetadata final tracksFromMetadata = filesWithMetadata
.map( .map(

View File

@ -23,6 +23,7 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
final subscription = final subscription =
database.select(database.scrobblerTable).watch().listen((event) async { database.select(database.scrobblerTable).watch().listen((event) async {
try {
if (event.isNotEmpty) { if (event.isNotEmpty) {
state = await AsyncValue.guard( state = await AsyncValue.guard(
() async => Scrobblenaut( () async => Scrobblenaut(
@ -37,6 +38,9 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
} else { } else {
state = const AsyncValue.data(null); state = const AsyncValue.data(null);
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
final scrobblerSubscription = final scrobblerSubscription =

View File

@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset); final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
return tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
return (
items: items,
hasMore: !tracks.isLast,
nextOffset: tracks.nextOffset,
);
} }
@override @override
@ -39,12 +45,12 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
ref.cacheFor(); ref.cacheFor();
ref.watch(spotifyProvider); ref.watch(spotifyProvider);
final tracks = await fetch(arg, 0, 20); final (:items, :nextOffset, :hasMore) = await fetch(arg, 0, 20);
return AlbumTracksState( return AlbumTracksState(
items: tracks, items: items,
offset: 0, offset: nextOffset,
limit: 20, limit: 20,
hasMore: tracks.length == 20, hasMore: hasMore,
); );
} }
} }

View File

@ -35,7 +35,13 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
.albums(arg, country: market) .albums(arg, country: market)
.getPage(limit, offset); .getPage(limit, offset);
return albums.items?.toList() ?? []; final items = albums.items?.toList() ?? [];
return (
items: items,
hasMore: !albums.isLast,
nextOffset: albums.nextOffset,
);
} }
@override @override
@ -46,12 +52,12 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
ref.watch( ref.watch(
userPreferencesProvider.select((s) => s.market), userPreferencesProvider.select((s) => s.market),
); );
final albums = await fetch(arg, 0, 20); final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
return ArtistAlbumsState( return ArtistAlbumsState(
items: albums, items: items,
offset: 0, offset: nextOffset,
limit: 20, limit: 20,
hasMore: albums.length == 20, hasMore: hasMore,
); );
} }
} }

View File

@ -39,7 +39,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
(json) => PlaylistsFeatured.fromJson(json), (json) => PlaylistsFeatured.fromJson(json),
).getPage(limit, offset); ).getPage(limit, offset);
return playlists.items?.whereNotNull().toList() ?? []; final items = playlists.items?.whereNotNull().toList() ?? [];
return (
items: items,
hasMore: !playlists.isLast,
nextOffset: playlists.nextOffset,
);
} }
@override @override
@ -50,13 +56,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
ref.watch(userPreferencesProvider.select((s) => s.locale)); ref.watch(userPreferencesProvider.select((s) => s.locale));
ref.watch(userPreferencesProvider.select((s) => s.market)); 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( return CategoryPlaylistsState(
items: playlists, items: items,
offset: 0, offset: nextOffset,
limit: 8, limit: 8,
hasMore: playlists.length == 8, hasMore: hasMore,
); );
} }
} }

View File

@ -125,6 +125,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
try { try {
final database = ref.watch(databaseProvider); final database = ref.watch(databaseProvider);
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final auth = await ref.watch(authenticationProvider.future);
if (track == null) { if (track == null) {
throw "No track currently"; throw "No track currently";
@ -139,11 +140,13 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
final token = await spotify.getCredentials(); final token = await spotify.getCredentials();
if (lyrics == null || lyrics.lyrics.isEmpty) { if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) {
lyrics = await getSpotifyLyrics(token.accessToken); 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(); lyrics = await getLRCLibLyrics();
} }

View File

@ -36,10 +36,16 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
/// Filter out tracks with null id because some personal playlists /// Filter out tracks with null id because some personal playlists
/// may contain local tracks that are not available in the Spotify catalog /// 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") ?.where((track) => track.id != null && track.type == "track")
.toList() ?? .toList() ??
<Track>[]; <Track>[];
return (
items: items,
hasMore: !tracks.isLast,
nextOffset: tracks.nextOffset,
);
} }
@override @override
@ -47,13 +53,13 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
ref.cacheFor(); ref.cacheFor();
ref.watch(spotifyProvider); ref.watch(spotifyProvider);
final tracks = await fetch(arg, 0, 20); final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
return PlaylistTracksState( return PlaylistTracksState(
items: tracks, items: tracks,
offset: 0, offset: nextOffset,
limit: 20, limit: 20,
hasMore: tracks.length == 20, hasMore: hasMore,
); );
} }
} }

View File

@ -37,7 +37,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
@override @override
fetch(arg, offset, limit) async { fetch(arg, offset, limit) async {
if (state.value == null) return []; if (state.value == null) {
return (
items: <Y>[],
hasMore: false,
nextOffset: 0,
);
}
final results = await spotify.search final results = await spotify.search
.get( .get(
ref.read(searchTermStateProvider), ref.read(searchTermStateProvider),
@ -46,7 +52,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
) )
.getPage(limit, offset); .getPage(limit, offset);
return results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>(); final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();
return (
items: items,
hasMore: items.length == limit,
nextOffset: offset + limit,
);
} }
@override @override
@ -59,13 +71,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
userPreferencesProvider.select((value) => value.market), userPreferencesProvider.select((value) => value.market),
); );
final results = await fetch(arg, 0, 10); final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10);
return SearchState<Y>( return SearchState<Y>(
items: results, items: items,
offset: 0, offset: nextOffset,
limit: 10, limit: 10,
hasMore: results.length == 10, hasMore: hasMore,
); );
} }
} }

View File

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:spotube/models/database/database.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/database/database.dart';
import 'package:spotube/provider/spotify/utils/json_cast.dart'; import 'package:spotube/provider/spotify/utils/json_cast.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';

View File

@ -1,10 +1,16 @@
part of '../../spotify.dart'; part of '../../spotify.dart';
typedef PseudoPaginatedProps<T> = ({
List<T> items,
int nextOffset,
bool hasMore,
});
abstract class FamilyPaginatedAsyncNotifier< abstract class FamilyPaginatedAsyncNotifier<
K, K,
T extends BasePaginatedState<K, dynamic>, T extends BasePaginatedState<K, dynamic>,
A> extends FamilyAsyncNotifier<T, A> with SpotifyMixin<T> { A> extends FamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
Future<List<K>> fetch(A arg, int offset, int limit); Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
@ -13,18 +19,18 @@ abstract class FamilyPaginatedAsyncNotifier<
state = await AsyncValue.guard( state = await AsyncValue.guard(
() async { () async {
final items = await fetch( final (:items, :hasMore, :nextOffset) = await fetch(
arg, arg,
state.value!.offset + state.value!.limit, state.value!.offset,
state.value!.limit, state.value!.limit,
); );
return state.value!.copyWith( return state.value!.copyWith(
hasMore: items.length == state.value!.limit, hasMore: hasMore,
items: [ items: [
...state.value!.items, ...state.value!.items,
...items, ...items,
], ],
offset: state.value!.offset + state.value!.limit, offset: nextOffset,
) as T; ) as T;
}, },
); );
@ -37,16 +43,16 @@ abstract class FamilyPaginatedAsyncNotifier<
bool hasMore = true; bool hasMore = true;
while (hasMore) { while (hasMore) {
await update((state) async { await update((state) async {
final items = await fetch( final res = await fetch(
arg, arg,
state.offset + state.limit, state.offset,
state.limit, state.limit,
); );
hasMore = items.length == state.limit; hasMore = res.hasMore;
return state.copyWith( return state.copyWith(
items: [...state.items, ...items], items: [...state.items, ...res.items],
offset: state.offset + state.limit, offset: res.nextOffset,
hasMore: hasMore, hasMore: hasMore,
) as T; ) as T;
}); });
@ -60,7 +66,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
K, K,
T extends BasePaginatedState<K, dynamic>, T extends BasePaginatedState<K, dynamic>,
A> extends AutoDisposeFamilyAsyncNotifier<T, A> with SpotifyMixin<T> { A> extends AutoDisposeFamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
Future<List<K>> fetch(A arg, int offset, int limit); Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
Future<void> fetchMore() async { Future<void> fetchMore() async {
if (state.value == null || !state.value!.hasMore) return; if (state.value == null || !state.value!.hasMore) return;
@ -69,18 +75,19 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
state = await AsyncValue.guard( state = await AsyncValue.guard(
() async { () async {
final items = await fetch( final (:items, :hasMore, :nextOffset) = await fetch(
arg, arg,
state.value!.offset + state.value!.limit, state.value!.offset,
state.value!.limit, state.value!.limit,
); );
return state.value!.copyWith( return state.value!.copyWith(
hasMore: items.length == state.value!.limit, hasMore: hasMore,
items: [ items: [
...state.value!.items, ...state.value!.items,
...items, ...items,
], ],
offset: state.value!.offset + state.value!.limit, offset: nextOffset,
) as T; ) as T;
}, },
); );
@ -93,16 +100,16 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
bool hasMore = true; bool hasMore = true;
while (hasMore) { while (hasMore) {
await update((state) async { await update((state) async {
final items = await fetch( final res = await fetch(
arg, arg,
state.offset + state.limit, state.offset,
state.limit, state.limit,
); );
hasMore = items.length == state.limit; hasMore = res.hasMore;
return state.copyWith( return state.copyWith(
items: [...state.items, ...items], items: [...state.items, ...res.items],
offset: state.offset + state.limit, offset: res.nextOffset,
hasMore: hasMore, hasMore: hasMore,
) as T; ) as T;
}); });

View File

@ -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/database/database.dart';
import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/services/audio_player/audio_player.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/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@ -41,15 +42,21 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
..where((tbl) => tbl.id.equals(0))) ..where((tbl) => tbl.id.equals(0)))
.watchSingle() .watchSingle()
.listen((event) async { .listen((event) async {
try {
state = event; state = event;
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setTitleBarStyle( await windowManager.setTitleBarStyle(
state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, state.systemTitleBar
? TitleBarStyle.normal
: TitleBarStyle.hidden,
); );
} }
await audioPlayer.setAudioNormalization(state.normalizeAudio); await audioPlayer.setAudioNormalization(state.normalizeAudio);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
ref.onDispose(() { ref.onDispose(() {

View File

@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/provider/audio_player/audio_player.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/mobile_audio_service.dart';
import 'package:spotube/services/audio_services/windows_audio_service.dart'; import 'package:spotube/services/audio_services/windows_audio_service.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
@ -30,8 +31,8 @@ class AudioServices with WidgetsBindingObserver {
kIsLinux ? 'spotube' : 'com.krtirtho.Spotube', kIsLinux ? 'spotube' : 'com.krtirtho.Spotube',
androidNotificationChannelName: 'Spotube', androidNotificationChannelName: 'Spotube',
androidNotificationOngoing: false, androidNotificationOngoing: false,
androidNotificationIcon: "drawable/ic_launcher_monochrome",
androidStopForegroundOnPause: false, androidStopForegroundOnPause: false,
androidNotificationIcon: "drawable/ic_launcher_monochrome",
androidNotificationChannelDescription: "Spotube Media Controls", androidNotificationChannelDescription: "Spotube Media Controls",
), ),
) )
@ -73,7 +74,7 @@ class AudioServices with WidgetsBindingObserver {
switch (state) { switch (state) {
case AppLifecycleState.detached: case AppLifecycleState.detached:
deactivateSession(); deactivateSession();
mobile?.stop(); audioPlayer.pause();
break; break;
default: default:
break; break;

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.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/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:media_kit/media_kit.dart' hide Track; 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 { class MobileAudioService extends BaseAudioHandler {
AudioSession? session; AudioSession? session;
@ -119,11 +122,12 @@ class MobileAudioService extends BaseAudioHandler {
@override @override
Future<void> onTaskRemoved() async { Future<void> onTaskRemoved() async {
await audioPlayerNotifier.stop(); await audioPlayer.pause();
return super.onTaskRemoved(); if (kIsAndroid) exit(0);
} }
Future<PlaybackState> _transformEvent() async { Future<PlaybackState> _transformEvent() async {
try {
return PlaybackState( return PlaybackState(
controls: [ controls: [
MediaControl.skipToPrevious, MediaControl.skipToPrevious,
@ -150,5 +154,9 @@ class MobileAudioService extends BaseAudioHandler {
? AudioProcessingState.loading ? AudioProcessingState.loading
: AudioProcessingState.ready, : AudioProcessingState.ready,
); );
} catch (e, stack) {
AppLogger.reportError(e, stack);
rethrow;
}
} }
} }

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectionCheckerService with WidgetsBindingObserver { class ConnectionCheckerService with WidgetsBindingObserver {
final _connectionStreamController = StreamController<bool>.broadcast(); final _connectionStreamController = StreamController<bool>.broadcast();
@ -16,6 +17,7 @@ class ConnectionCheckerService with WidgetsBindingObserver {
Timer? timer; Timer? timer;
onConnectivityChanged.listen((connected) { onConnectivityChanged.listen((connected) {
try {
if (!connected && timer == null) { if (!connected && timer == null) {
timer = Timer.periodic(const Duration(seconds: 30), (timer) async { timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
if (WidgetsBinding.instance.lifecycleState == if (WidgetsBinding.instance.lifecycleState ==
@ -28,6 +30,9 @@ class ConnectionCheckerService with WidgetsBindingObserver {
timer?.cancel(); timer?.cancel();
timer = null; timer = null;
} }
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}); });
} }

View File

@ -458,7 +458,7 @@ packages:
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
dbus: dbus:
dependency: "direct main" dependency: transitive
description: description:
name: dbus name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
@ -506,14 +506,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: draggable_scrollbar:
dependency: "direct main" dependency: "direct main"
description: description:
@ -822,54 +814,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" 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: flutter_launcher_icons:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1062,10 +1006,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15 sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.1.3" version: "14.2.7"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1271,14 +1215,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" 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: io:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1784,14 +1720,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
retry:
dependency: "direct main"
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
riverpod: riverpod:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
publish_to: "none" publish_to: "none"
version: 3.8.0+33 version: 3.8.1+34
homepage: https://spotube.krtirtho.dev homepage: https://spotube.krtirtho.dev
repository: https://github.com/KRTirtho/spotube repository: https://github.com/KRTirtho/spotube
@ -23,7 +23,6 @@ dependencies:
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
collection: ^1.15.0 collection: ^1.15.0
curved_navigation_bar: ^1.0.3 curved_navigation_bar: ^1.0.3
dbus: ^0.7.8
desktop_webview_window: desktop_webview_window:
git: git:
url: https://github.com/KRTirtho/flutter-plugins.git url: https://github.com/KRTirtho/flutter-plugins.git
@ -52,7 +51,6 @@ dependencies:
flutter_svg: ^1.1.6 flutter_svg: ^1.1.6
form_validator: ^2.1.1 form_validator: ^2.1.1
fuzzywuzzy: ^1.1.6 fuzzywuzzy: ^1.1.6
go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869
google_fonts: ^6.2.1 google_fonts: ^6.2.1
hive: ^2.2.3 hive: ^2.2.3
hive_flutter: ^1.1.0 hive_flutter: ^1.1.0
@ -60,7 +58,6 @@ dependencies:
html: ^0.15.1 html: ^0.15.1
image_picker: ^1.1.0 image_picker: ^1.1.0
intl: any intl: any
introduction_screen: ^3.1.14
json_annotation: ^4.8.1 json_annotation: ^4.8.1
logger: ^2.0.2 logger: ^2.0.2
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1
@ -137,7 +134,7 @@ dependencies:
sqlite3_flutter_libs: ^0.5.23 sqlite3_flutter_libs: ^0.5.23
sqlite3: ^2.4.3 sqlite3: ^2.4.3
encrypt: ^5.0.3 encrypt: ^5.0.3
retry: ^3.1.2 go_router: ^14.2.7
dev_dependencies: dev_dependencies:
build_runner: ^2.4.9 build_runner: ^2.4.9

View File

@ -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

View File

@ -20,6 +20,7 @@ Compression=lzma
SolidCompression=yes SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}} SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern WizardStyle=modern
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
PrivilegesRequired={{PRIVILEGES_REQUIRED}} PrivilegesRequired={{PRIVILEGES_REQUIRED}}
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64