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

View File

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

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.
## [3.8.1](https://github.com/krtirtho/spotube/compare/v3.8.0...v3.8.1) (2024-09-15)
## Changes
### Bug Fixes
- **translations**: correct some basque incorrect translations (#1815)
- **lyrics**: LRCLIB lyrics should be usable without logging in #1803
- playlist displaying descriptions unescaped html #1784
- **android**: pressing back while the player is open doesn't take to previous page
- handle dublicated items in playback queue correctly #1852
- **desktop**: scrollbar overlapping with more options of tracks and playlists
- **discord**: stop discord rpc from try update presence when not connected
- **stats**: minutes page shows plays and streams page shows minutes which should be the opposite #1880
- **android**: clears queue upon swiping away notification
- **player**: shuffle button state resets after closing page #1657
- getting started page login page exception #1800
- **mobile**: queue doesn't persist
- local tracks takes time to load
- start radio not working #1629
### Features
- **desktop**: show error dialog if webview is not found on login #1871
- manually detect and define touch behavior #1763
## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06)
### Features

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

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;
/// 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");
return _panelState!._close();
await _panelState!._close();
notifyListeners();
}
/// Opens the sliding panel fully
/// (i.e. to the maxHeight)
Future<void> open() {
Future<void> open() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._open();
await _panelState!._open();
notifyListeners();
}
/// Hides the sliding panel (i.e. is invisible)
Future<void> hide() {
Future<void> hide() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._hide();
await _panelState!._hide();
notifyListeners();
}
/// Shows the sliding panel in its collapsed state
/// (i.e. "un-hide" the sliding panel)
Future<void> show() {
Future<void> show() async {
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
return _panelState!._show();
await _panelState!._show();
notifyListeners();
}
/// Animates the panel position to the value.

View File

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

View File

@ -105,7 +105,9 @@ class TrackOptions extends HookConsumerWidget {
final pages =
await spotify.search.get(query, types: [SearchType.playlist]).first();
final radios = pages.map((e) => e.items).toList().cast<PlaylistSimple>();
final radios = pages
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
.toList();
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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
@ -21,6 +22,7 @@ import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class TrackTile extends HookConsumerWidget {
@ -276,6 +278,7 @@ class TrackTile extends HookConsumerWidget {
userPlaylist: userPlaylist,
showMenuCbRef: showOptionCbRef,
),
if (kIsDesktop) const Gap(10),
],
),
),

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/track_view_props.dart';
import 'package:spotube/components/tracks_view/track_view_provider.dart';
import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:spotube/provider/history/history.dart';
@ -65,6 +66,56 @@ class TrackViewBodySection extends HookConsumerWidget {
final isActive = playlist.collections.contains(props.collectionId);
final onTapTrackTile = useCallback((Track track, int index) async {
if (trackViewState.isSelecting) {
trackViewState.toggleTrackSelection(track.id!);
return;
}
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
if (isRemoteDevice) {
final remotePlayback = ref.read(connectProvider.notifier);
final remoteQueue = ref.read(queueProvider);
if (remoteQueue.collections.contains(props.collectionId) ||
remoteQueue.tracks.any((s) => s.id == track.id)) {
await playlistNotifier.jumpToTrack(track);
} else {
final tracks = await props.pagination.onFetchAll();
await remotePlayback.load(
props.collection is AlbumSimple
? WebSocketLoadEventData.album(
tracks: tracks,
collection: props.collection as AlbumSimple,
initialIndex: index,
)
: WebSocketLoadEventData.playlist(
tracks: tracks,
collection: props.collection as PlaylistSimple,
initialIndex: index,
),
);
}
} else {
if (isActive || playlist.tracks.containsBy(track, (a) => a.id)) {
await playlistNotifier.jumpToTrack(track);
} else {
final tracks = await props.pagination.onFetchAll();
await playlistNotifier.load(
tracks,
initialIndex: index,
autoPlay: true,
);
playlistNotifier.addCollection(props.collectionId);
if (props.collection is AlbumSimple) {
historyNotifier.addAlbums([props.collection as AlbumSimple]);
} else {
historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
}
}
}
}, [isActive, playlist, props, playlistNotifier, historyNotifier]);
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
@ -130,58 +181,7 @@ class TrackViewBodySection extends HookConsumerWidget {
trackViewState.selectTrack(track.id!);
HapticFeedback.selectionClick();
},
onTap: () async {
if (trackViewState.isSelecting) {
trackViewState.toggleTrackSelection(track.id!);
return;
}
final isRemoteDevice =
await showSelectDeviceDialog(context, ref);
if (isRemoteDevice) {
final remotePlayback = ref.read(connectProvider.notifier);
final remoteQueue = ref.read(queueProvider);
if (remoteQueue.collections.contains(props.collectionId) ||
remoteQueue.tracks.any((s) => s.id == track.id)) {
await playlistNotifier.jumpToTrack(track);
} else {
final tracks = await props.pagination.onFetchAll();
await remotePlayback.load(
props.collection is AlbumSimple
? WebSocketLoadEventData.album(
tracks: tracks,
collection: props.collection as AlbumSimple,
initialIndex: index,
)
: WebSocketLoadEventData.playlist(
tracks: tracks,
collection: props.collection as PlaylistSimple,
initialIndex: index,
),
);
}
} else {
if (isActive || playlist.tracks.contains(track)) {
await playlistNotifier.jumpToTrack(track);
} else {
final tracks = await props.pagination.onFetchAll();
await playlistNotifier.load(
tracks,
initialIndex: index,
autoPlay: true,
);
playlistNotifier.addCollection(props.collectionId);
if (props.collection is AlbumSimple) {
historyNotifier
.addAlbums([props.collection as AlbumSimple]);
} else {
historyNotifier
.addPlaylists([props.collection as PlaylistSimple]);
}
}
}
},
onTap: () => onTapTrackTile(track, index),
);
},
),

View File

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

View File

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

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/parser.dart';
final htmlEscape = HtmlUnescape();
extension UnescapeHtml on String {
String cleanHtml() => parse("<p>$this</p>").documentElement!.text;
String unescapeHtml() => htmlEscape.convert(this);
}
extension NullableUnescapeHtml on String? {
String? cleanHtml() => this?.cleanHtml();
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:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';
final appLinks = AppLinks();
@ -61,6 +62,7 @@ void useDeepLinking(WidgetRef ref) {
}
final subscription = linkStream.listen((uri) async {
try {
final startSegment = uri.split(":").take(2).join(":");
final endSegment = uri.split(":").last;
@ -86,6 +88,9 @@ void useDeepLinking(WidgetRef ref) {
default:
break;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
});
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_playlists": "قوائم التشغيل",
"summary_were_on_repeat": "كانت على التكرار",
"total_money": "المجموع {money}"
"total_money": "المجموع {money}",
"webview_not_found": "لم يتم العثور على Webview",
"webview_not_found_description": "لم يتم تثبيت بيئة تشغيل Webview على جهازك.\nإذا كانت مثبتة، تأكد من وجودها في environment PATH\n\nبعد التثبيت، أعد تشغيل التطبيق",
"unsupported_platform": "المنصة غير مدعومة"
}

View File

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

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "ha aconseguit el teu amor",
"summary_playlists": "llistes de reproducció",
"summary_were_on_repeat": "estaven en repetició",
"total_money": "total {money}"
"total_money": "total {money}",
"webview_not_found": "No s'ha trobat el Webview",
"webview_not_found_description": "No hi ha cap temps d'execució de Webview instal·lat al dispositiu.\nSi està instal·lat, assegureu-vos que estigui en el environment PATH\n\nDesprés d'instal·lar-lo, reinicieu l'aplicació",
"unsupported_platform": "Plataforma no compatible"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Získal vaši lásku",
"summary_playlists": "playlisty",
"summary_were_on_repeat": "Byly na opakování",
"total_money": "Celkem {money}"
"total_money": "Celkem {money}",
"webview_not_found": "Webview nebyl nalezen",
"webview_not_found_description": "Na vašem zařízení není nainstalováno žádné runtime prostředí Webview.\nPokud je nainstalováno, ujistěte se, že je v environment PATH\n\nPo instalaci restartujte aplikaci",
"unsupported_platform": "Nepodporovaná platforma"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Hat Ihre Liebe gewonnen",
"summary_playlists": "Wiedergabelisten",
"summary_were_on_repeat": "Wurden wiederholt",
"total_money": "Gesamt {money}"
"total_money": "Gesamt {money}",
"webview_not_found": "Webview nicht gefunden",
"webview_not_found_description": "Es ist keine Webview-Laufzeitumgebung auf Ihrem Gerät installiert.\nFalls installiert, stellen Sie sicher, dass es im environment PATH ist\n\nNach der Installation starten Sie die App neu",
"unsupported_platform": "Nicht unterstützte Plattform"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Got your love",
"summary_playlists": "playlists",
"summary_were_on_repeat": "Were on repeat",
"total_money": "Total {money}"
"total_money": "Total {money}",
"webview_not_found": "Webview not found",
"webview_not_found_description": "No webview runtime is installed in your device.\nIf it's installed make sure it's in the Environment PATH\n\nAfter installing, restart the app",
"unsupported_platform": "Unsupported platform"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Obtuvo tu amor",
"summary_playlists": "listas de reproducción",
"summary_were_on_repeat": "Estaban en repetición",
"total_money": "Total {money}"
"total_money": "Total {money}",
"webview_not_found": "No se encontró el Webview",
"webview_not_found_description": "No hay tiempo de ejecución de Webview instalado en su dispositivo.\nSi está instalado, asegúrese de que esté en el environment PATH\n\nDespués de instalar, reinicie la aplicación",
"unsupported_platform": "Plataforma no soportada"
}

View File

@ -367,7 +367,7 @@
"count_plays": "{count} erreprodukzio",
"streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)",
"minutes_listened": "Entzundako minutuak",
"streamed_songs": "Stream-eatutako kantak",
"streamed_songs": "Streaming-ez entzundako kantak",
"count_streams": "{count} stream",
"owned_by_you": "Zure jabetzakoa",
"copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua",
@ -376,13 +376,16 @@
"summary_minutes": "minutu",
"summary_listened_to_music": "Musika entzuten",
"summary_songs": "kanta",
"summary_streamed_overall": "Stream-eatuta oro har",
"summary_streamed_overall": "Streaming abesti oro har",
"summary_owed_to_artists": "Hilabete honetan\nartistei zor zaiena",
"summary_artists": "artisten",
"summary_music_reached_you": "Musika ailegatu zaizu",
"summary_full_albums": "album osok",
"summary_got_your_love": "Izan dute zure maitasuna",
"summary_got_your_love": "Jaso dute zure maitasuna",
"summary_playlists": "zerrenda",
"summary_were_on_repeat": "Dituzu errepikatze moduan",
"total_money": "Guztira {money}"
"total_money": "Guztira {money}",
"webview_not_found": "Ez da Webview aurkitu",
"webview_not_found_description": "Ez dago Webview abiarazte denbora-instalaziorik zure gailuan.\nInstalatuta badago, ziurtatu environment PATH-an dagoela\n\nInstalatu ondoren, berrabiarazi aplikazioa",
"unsupported_platform": "Plataforma ez onartua"
}

View File

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

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Sai rakkautesi",
"summary_playlists": "soittolistat",
"summary_were_on_repeat": "Olivat toistossa",
"total_money": "Yhteensä {money}"
"total_money": "Yhteensä {money}",
"webview_not_found": "Webview ei löydy",
"webview_not_found_description": "Laitteellasi ei ole asennettua Webview-ajonaikaa.\nJos se on asennettu, varmista, että se on environment PATH:ssa\n\nAsennuksen jälkeen käynnistä sovellus uudelleen",
"unsupported_platform": "Ei tuettu alusta"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "A obtenu votre amour",
"summary_playlists": "playlists",
"summary_were_on_repeat": "Était en répétition",
"total_money": "Total {money}"
"total_money": "Total {money}",
"webview_not_found": "Webview non trouvé",
"webview_not_found_description": "Aucun environnement d'exécution Webview installé sur votre appareil.\nSi c'est installé, assurez-vous qu'il soit dans le environment PATH\n\nAprès l'installation, redémarrez l'application",
"unsupported_platform": "Plateforme non prise en charge"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} स्ट्रिम",
"owned_by_you": "तपाईंले स्वामित्व गरेको",
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो",
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।"
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबव्यू नहीं मिला",
"webview_not_found_description": "आपके डिवाइस पर वेबव्यू रनटाइम इंस्टॉल नहीं है।\nअगर इंस्टॉल है, तो सुनिश्चित करें कि यह environment PATH में है\n\nइंस्टॉल करने के बाद, ऐप को पुनः शुरू करें",
"unsupported_platform": "असमर्थित प्लेटफार्म"
}

View File

@ -384,5 +384,8 @@
"summary_got_your_love": "Mendapatkan cinta Anda",
"summary_playlists": "daftar putar",
"summary_were_on_repeat": "Sedang diulang",
"total_money": "Total {money}"
"total_money": "Total {money}",
"webview_not_found": "Webview tidak ditemukan",
"webview_not_found_description": "Tidak ada runtime Webview yang diinstal di perangkat Anda.\nJika sudah diinstal, pastikan itu ada di environment PATH\n\nSetelah diinstal, restart aplikasi",
"unsupported_platform": "Platform tidak didukung"
}

View File

@ -385,5 +385,8 @@
"summary_got_your_love": "Ha ricevuto il tuo amore",
"summary_playlists": "playlist",
"summary_were_on_repeat": "Erano in ripetizione",
"total_money": "Totale {money}"
"total_money": "Totale {money}",
"webview_not_found": "Webview non trovato",
"webview_not_found_description": "Nessun runtime Webview installato nel tuo dispositivo.\nSe è installato, assicurati che sia nel environment PATH\n\nDopo l'installazione, riavvia l'app",
"unsupported_platform": "Piattaforma non supportata"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} 回のストリーム",
"owned_by_you": "あなたが所有",
"copied_shareurl_to_clipboard": "{shareUrl} をクリップボードにコピーしました",
"spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。"
"spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。",
"webview_not_found": "Webviewが見つかりません",
"webview_not_found_description": "デバイスにWebviewランタイムがインストールされていません。\nインストールされている場合は、environment PATHにあることを確認してください\n\nインストール後、アプリを再起動してください",
"unsupported_platform": "サポートされていないプラットフォーム"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} სტრიმი",
"owned_by_you": "შენ მიერ საკუთრებული",
"copied_shareurl_to_clipboard": "{shareUrl} აიღო კლიპბორდზე",
"spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე."
"spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე.",
"webview_not_found": "ვებვიუ ვერ მოიძებნა",
"webview_not_found_description": "თქვენს მოწყობილობაზე ვებვიუის შესრულების დრო არ არის დაყენებული.\nთუ დაყენებულია, დარწმუნდით, რომ ის environment PATH-შია\n\nდაენების შემდეგ, გადატვირთეთ აპი",
"unsupported_platform": "მოუხერხებელი პლატფორმა"
}

View File

@ -385,5 +385,8 @@
"count_streams": "{count} 스트림",
"owned_by_you": "당신이 소유",
"copied_shareurl_to_clipboard": "{shareUrl}를 클립보드에 복사했습니다",
"spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다."
"spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다.",
"webview_not_found": "웹뷰를 찾을 수 없음",
"webview_not_found_description": "기기에 웹뷰 런타임이 설치되지 않았습니다.\n설치되어 있으면 environment PATH에 있는지 확인하십시오\n\n설치 후 앱을 다시 시작하세요",
"unsupported_platform": "지원되지 않는 플랫폼"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} स्ट्रिम",
"owned_by_you": "तपाईंले स्वामित्व गरेको",
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो",
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।"
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
"webview_not_found": "वेबभ्यू फेला परेन",
"webview_not_found_description": "तपाईंको उपकरणमा कुनै वेबभ्यू रनटाइम स्थापना गरिएको छैन।\nयदि स्थापना गरिएको छ भने, environment PATH मा छ कि छैन भनेर सुनिश्चित गर्नुहोस्\n\nस्थापना पछि, अनुप्रयोग पुनः सुरु गर्नुहोस्",
"unsupported_platform": "असमर्थित प्लेटफार्म"
}

View File

@ -385,5 +385,8 @@
"count_streams": "{count} streams",
"owned_by_you": "Bezit door jou",
"copied_shareurl_to_clipboard": "{shareUrl} gekopieerd naar klembord",
"spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren."
"spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren.",
"webview_not_found": "Webview niet gevonden",
"webview_not_found_description": "Er is geen Webview-runtime geïnstalleerd op uw apparaat.\nAls het is geïnstalleerd, zorg ervoor dat het in het environment PATH staat\n\nHerstart de app na installatie",
"unsupported_platform": "Niet ondersteund platform"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} strumieni",
"owned_by_you": "Własność Twoja",
"copied_shareurl_to_clipboard": "{shareUrl} skopiowano do schowka",
"spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify."
"spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify.",
"webview_not_found": "Nie znaleziono Webview",
"webview_not_found_description": "Na twoim urządzeniu nie zainstalowano środowiska uruchomieniowego Webview.\nJeśli jest zainstalowany, upewnij się, że jest w environment PATH\n\nPo instalacji uruchom ponownie aplikację",
"unsupported_platform": "Nieobsługiwana platforma"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} streams",
"owned_by_you": "De sua propriedade",
"copied_shareurl_to_clipboard": "{shareUrl} copiado para a área de transferência",
"spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify."
"spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify.",
"webview_not_found": "Webview não encontrado",
"webview_not_found_description": "Nenhum runtime Webview está instalado no seu dispositivo.\nSe estiver instalado, certifique-se de que está no environment PATH\n\nApós a instalação, reinicie o aplicativo",
"unsupported_platform": "Plataforma não suportada"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} стримов",
"owned_by_you": "Ваша собственность",
"copied_shareurl_to_clipboard": "{shareUrl} скопировано в буфер обмена",
"spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify."
"spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.",
"webview_not_found": "Webview не найден",
"webview_not_found_description": "На вашем устройстве не установлена среда выполнения Webview.\nЕсли он установлен, убедитесь, что он находится в environment PATH\n\nПосле установки перезапустите приложение",
"unsupported_platform": "Платформа не поддерживается"
}

View File

@ -385,5 +385,8 @@
"count_streams": "{count} สตรีม",
"owned_by_you": "เป็นเจ้าของโดยคุณ",
"copied_shareurl_to_clipboard": "{shareUrl} คัดลอกไปที่คลิปบอร์ดแล้ว",
"spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify."
"spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify.",
"webview_not_found": "ไม่พบ Webview",
"webview_not_found_description": "ไม่พบ runtime ของ Webview บนอุปกรณ์ของคุณ\nหากติดตั้งแล้วตรวจสอบให้แน่ใจว่าอยู่ใน environment PATH\n\nหลังจากติดตั้งแล้ว ให้รีสตาร์ทแอป",
"unsupported_platform": "แพลตฟอร์มไม่รองรับ"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} yayın",
"owned_by_you": "Sahip olduğunuz",
"copied_shareurl_to_clipboard": "{shareUrl} panoya kopyalandı",
"spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir."
"spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir.",
"webview_not_found": "Webview bulunamadı",
"webview_not_found_description": "Cihazınızda herhangi bir Webview çalışma zamanı yüklü değil.\nEğer kuruluysa, ortam YOLUNDA olduğundan emin olun\n\nKurulumdan sonra uygulamayı yeniden başlatın",
"unsupported_platform": "Desteklenmeyen platform"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} стримів",
"owned_by_you": "Ваша власність",
"copied_shareurl_to_clipboard": "{shareUrl} скопійовано в буфер обміну",
"spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify."
"spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify.",
"webview_not_found": "Webview не знайдено",
"webview_not_found_description": "На вашому пристрої не встановлено виконуване середовище Webview.\nЯкщо воно встановлено, переконайтеся, що воно знаходиться в environment PATH\n\nПісля встановлення перезапустіть програму",
"unsupported_platform": "Непідтримувана платформа"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} lượt phát",
"owned_by_you": "Thuộc sở hữu của bạn",
"copied_shareurl_to_clipboard": "{shareUrl} đã sao chép vào bảng tạm",
"spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify."
"spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify.",
"webview_not_found": "Không tìm thấy Webview",
"webview_not_found_description": "Không có runtime Webview nào được cài đặt trên thiết bị của bạn.\nNếu đã cài đặt, hãy đảm bảo rằng nó nằm trong environment PATH\n\nSau khi cài đặt, hãy khởi động lại ứng dụng",
"unsupported_platform": "Nền tảng không được hỗ trợ"
}

View File

@ -384,5 +384,8 @@
"count_streams": "{count} 次流媒体",
"owned_by_you": "由您拥有",
"copied_shareurl_to_clipboard": "{shareUrl} 已复制到剪贴板",
"spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。"
"spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。",
"webview_not_found": "未找到 Webview",
"webview_not_found_description": "您的设备中未安装 Webview 运行时。\n如果已安装请确保它在 environment PATH 中\n\n安装后重新启动应用程序",
"unsupported_platform": "不支持的平台"
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ui';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.dart';
@ -22,6 +23,7 @@ import 'package:spotube/hooks/configurators/use_deep_linking.dart';
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
import 'package:spotube/hooks/configurators/use_fix_window_stretching.dart';
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
import 'package:spotube/hooks/configurators/use_has_touch.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
import 'package:spotube/provider/database/database.dart';
@ -142,6 +144,7 @@ class Spotube extends HookConsumerWidget {
final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider);
final hasTouchSupport = useHasTouch();
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
ref.listen(bonsoirProvider, (_, __) {});
@ -191,8 +194,22 @@ class Spotube extends HookConsumerWidget {
debugShowCheckedModeBanner: false,
title: 'Spotube',
builder: (context, child) {
if (kIsDesktop && !kIsMacOS) return DragToResizeArea(child: child!);
return child!;
child = ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: hasTouchSupport
? {
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
}
: null,
),
child: child!,
);
if (kIsDesktop && !kIsMacOS) child = DragToResizeArea(child: child);
return child;
},
themeMode: themeMode,
theme: lightTheme,

View File

@ -1,6 +1,7 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
int useSyncedLyrics(
WidgetRef ref,
@ -13,9 +14,13 @@ int useSyncedLyrics(
useEffect(() {
return stream.listen((pos) {
try {
if (lyricsMap.containsKey(pos.inSeconds + delay)) {
currentTime.value = pos.inSeconds + delay;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}).cancel;
}, [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/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/player_queue.dart';
@ -100,11 +101,10 @@ class PlayerView extends HookConsumerWidget {
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () async {
return AppPopScope(
canPop: context.canPop(),
onPopInvoked: (didPop) async {
await panelController.close();
return false;
},
child: IconTheme(
data: theme.iconTheme.copyWith(color: bodyTextColor),

View File

@ -170,10 +170,9 @@ class PlayerControls extends HookConsumerWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
Consumer(builder: (context, ref, _) {
final shuffled = ref
.watch(audioPlayerProvider.select((s) => s.shuffled));
return IconButton(
tooltip: shuffled
? 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/hooks/utils/use_brightness_value.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -30,7 +29,6 @@ class BottomPlayer extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
final playlist = ref.watch(audioPlayerProvider);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
@ -89,7 +87,6 @@ class BottomPlayer extends HookConsumerWidget {
children: [
PlayerActions(
extraActions: [
if (auth.asData?.value != null)
IconButton(
tooltip: context.l10n.mini_player,
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/extensions/context.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -16,6 +16,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final onLogin = useLoginCallback(ref);
return Center(
child: Column(
@ -121,9 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
),
onPressed: () async {
await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) {
context.pushNamed(WebViewLogin.name);
}
await onLogin();
},
),
],

View File

@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/themed_button_tab_bar.dart';
@ -17,7 +16,6 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@ -82,15 +80,6 @@ class LyricsPage extends HookConsumerWidget {
),
);
final auth = ref.watch(authenticationProvider);
if (auth.asData?.value == null) {
return Scaffold(
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
body: const AnonymousFallback(),
);
}
if (isModal) {
return DefaultTabController(
length: 2,

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

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

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(
initialSettings: InAppWebViewSettings(
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 afari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
),
initialUrlRequest: URLRequest(
url: WebUri("https://accounts.spotify.com/"),

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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/modules/root/bottom_player.dart';
@ -30,10 +32,11 @@ class RootApp extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final showingDialogCompleter = useRef(Completer()..complete());
final downloader = ref.watch(downloadManagerProvider);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final theme = Theme.of(context);
final connectRoutes = ref.watch(serverConnectRoutesProvider);
useEffect(() {
@ -164,17 +167,11 @@ class RootApp extends HookConsumerWidget {
return null;
}, [backgroundColor]);
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () async {
final routerState = GoRouterState.of(context);
if (routerState.matchedLocation != "/") {
context.goNamed(HomePage.name);
return false;
}
return true;
},
child: Scaffold(
final navTileNames = useMemoized(() {
return getSidebarTileList(context.l10n).map((s) => s.name).toList();
}, []);
final scaffold = Scaffold(
body: Sidebar(child: child),
extendBody: true,
drawerScrimColor: Colors.transparent,
@ -212,7 +209,27 @@ class RootApp extends HookConsumerWidget {
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/extensions/context.dart';
import 'package:spotube/provider/logs/logs_provider.dart';
import 'package:spotube/services/logger/logger.dart';
class LogsPage extends HookConsumerWidget {
static const name = "logs";
@ -40,6 +41,17 @@ class LogsPage extends HookConsumerWidget {
}
},
),
IconButton(
icon: const Icon(SpotubeIcons.trash),
iconSize: 16,
onPressed: () async {
ref.invalidate(logsProvider);
final logsFile = await AppLogger.getLogsPath();
await logsFile.writeAsString("");
},
)
],
),
body: SafeArea(

View File

@ -1,24 +1,18 @@
import 'dart:io';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/mobile_login/mobile_login.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
class SettingsAccountSection extends HookConsumerWidget {
@ -30,7 +24,6 @@ class SettingsAccountSection extends HookConsumerWidget {
final router = GoRouter.of(context);
final auth = ref.watch(authenticationProvider);
final authNotifier = ref.watch(authenticationProvider.notifier);
final scrobbler = ref.watch(scrobblerProvider);
final me = ref.watch(meProvider);
final meData = me.asData?.value;
@ -40,51 +33,7 @@ class SettingsAccountSection extends HookConsumerWidget {
foregroundColor: Colors.white,
);
void onLogin() async {
if (kIsMobile) {
router.pushNamed(WebViewLogin.name);
return;
}
final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
final applicationSupportDir = await getApplicationSupportDirectory();
final userDataFolder = Directory(
join(applicationSupportDir.path, "webview_window_Webview2"));
if (!await userDataFolder.exists()) {
await userDataFolder.create();
}
final webview = await WebviewWindow.create(
configuration: CreateConfiguration(
title: "Spotify Login",
titleBarTopPadding: kIsMacOS ? 20 : 0,
windowHeight: 720,
windowWidth: 1280,
userDataFolderWindows: userDataFolder.path,
),
);
webview
..setBrightness(theme.colorScheme.brightness)
..launch("https://accounts.spotify.com/")
..setOnUrlRequestCallback((url) {
if (exp.hasMatch(url)) {
webview.getAllCookies().then((cookies) async {
final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}";
await authNotifier.login(cookieHeader);
webview.close();
if (context.mounted) {
context.go("/");
}
});
}
return true;
});
}
final onLogin = useLoginCallback(ref);
return SectionCardWithHeading(
heading: context.l10n.account,

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:bonsoir/bonsoir.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/device_info/device_info.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectClientsState {
final List<BonsoirService> services;
@ -37,6 +38,7 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
final subscription = discovery.eventStream?.listen((event) {
// ignore device itself
try {
if (event.service?.attributes["deviceId"] == deviceId) {
return;
}
@ -65,7 +67,8 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
.toList(),
discovery: state.value!.discovery,
resolvedService: state.value?.resolvedService != null &&
event.service?.name == state.value?.resolvedService?.name
event.service?.name ==
state.value?.resolvedService?.name
? null
: state.value!.resolvedService,
),
@ -74,6 +77,9 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
default:
break;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
});
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/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';
class DiscordNotifier extends AsyncNotifier<void> {
@override
FutureOr<void> build() async {
if (!kIsDesktop) return;
final enabled = ref.watch(
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
@ -19,18 +22,27 @@ class DiscordNotifier extends AsyncNotifier<void> {
final subscriptions = [
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
try {
final playback = ref.read(audioPlayerProvider);
if (connected && playback.activeTrack != null) {
await updatePresence(playback.activeTrack!);
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}),
audioPlayer.playerStateStream.listen((state) async {
try {
final playback = ref.read(audioPlayerProvider);
if (playback.activeTrack == null) return;
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
}),
audioPlayer.positionStream.listen((position) async {
try {
final playback = ref.read(audioPlayerProvider);
if (playback.activeTrack != null) {
final diff = position.inMilliseconds - lastPosition.inMilliseconds;
@ -39,6 +51,9 @@ class DiscordNotifier extends AsyncNotifier<void> {
}
}
lastPosition = position;
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
})
];
@ -46,6 +61,7 @@ class DiscordNotifier extends AsyncNotifier<void> {
for (final subscription in subscriptions) {
subscription.cancel();
}
await clear();
await close();
await FlutterDiscordRPC.instance.dispose();
});
@ -53,12 +69,14 @@ class DiscordNotifier extends AsyncNotifier<void> {
if (!enabled && FlutterDiscordRPC.instance.isConnected) {
await clear();
await close();
} else {
} else if (enabled) {
await FlutterDiscordRPC.instance.connect(autoRetry: true);
}
}
Future<void> updatePresence(Track track) async {
if (!kIsDesktop) return;
if (FlutterDiscordRPC.instance.isConnected == false) return;
final artistNames = track.artists?.asString();
final isPlaying = audioPlayer.isPlaying;
final position = audioPlayer.position;
@ -92,10 +110,12 @@ class DiscordNotifier extends AsyncNotifier<void> {
}
Future<void> clear() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.clearActivity();
}
Future<void> close() async {
if (!kIsDesktop) return;
await FlutterDiscordRPC.instance.disconnect();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
@override
fetch(arg, offset, limit) async {
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
@ -39,12 +45,12 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
ref.cacheFor();
ref.watch(spotifyProvider);
final tracks = await fetch(arg, 0, 20);
final (:items, :nextOffset, :hasMore) = await fetch(arg, 0, 20);
return AlbumTracksState(
items: tracks,
offset: 0,
items: items,
offset: nextOffset,
limit: 20,
hasMore: tracks.length == 20,
hasMore: hasMore,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
@override
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
.get(
ref.read(searchTermStateProvider),
@ -46,7 +52,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
)
.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
@ -59,13 +71,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
userPreferencesProvider.select((value) => value.market),
);
final results = await fetch(arg, 0, 10);
final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10);
return SearchState<Y>(
items: results,
offset: 0,
items: items,
offset: nextOffset,
limit: 10,
hasMore: results.length == 10,
hasMore: hasMore,
);
}
}

View File

@ -4,6 +4,7 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/spotify/utils/json_cast.dart';
import 'package:spotube/services/logger/logger.dart';

View File

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

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

View File

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

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
@ -6,6 +7,8 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';
class MobileAudioService extends BaseAudioHandler {
AudioSession? session;
@ -119,11 +122,12 @@ class MobileAudioService extends BaseAudioHandler {
@override
Future<void> onTaskRemoved() async {
await audioPlayerNotifier.stop();
return super.onTaskRemoved();
await audioPlayer.pause();
if (kIsAndroid) exit(0);
}
Future<PlaybackState> _transformEvent() async {
try {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
@ -150,5 +154,9 @@ class MobileAudioService extends BaseAudioHandler {
? AudioProcessingState.loading
: 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:flutter/widgets.dart';
import 'package:spotube/services/logger/logger.dart';
class ConnectionCheckerService with WidgetsBindingObserver {
final _connectionStreamController = StreamController<bool>.broadcast();
@ -16,6 +17,7 @@ class ConnectionCheckerService with WidgetsBindingObserver {
Timer? timer;
onConnectivityChanged.listen((connected) {
try {
if (!connected && timer == null) {
timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
if (WidgetsBinding.instance.lifecycleState ==
@ -28,6 +30,9 @@ class ConnectionCheckerService with WidgetsBindingObserver {
timer?.cancel();
timer = null;
}
} catch (e, stack) {
AppLogger.reportError(e, stack);
}
});
}

View File

@ -458,7 +458,7 @@ packages:
source: hosted
version: "1.2.0"
dbus:
dependency: "direct main"
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
@ -506,14 +506,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
dots_indicator:
dependency: transitive
description:
name: dots_indicator
sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c
url: "https://pub.dev"
source: hosted
version: "2.1.2"
draggable_scrollbar:
dependency: "direct main"
description:
@ -822,54 +814,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
flutter_keyboard_visibility:
dependency: transitive
description:
name: flutter_keyboard_visibility
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_keyboard_visibility_linux:
dependency: transitive
description:
name: flutter_keyboard_visibility_linux
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_macos:
dependency: transitive
description:
name: flutter_keyboard_visibility_macos
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_web:
dependency: transitive
description:
name: flutter_keyboard_visibility_web
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
url: "https://pub.dev"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_windows:
dependency: transitive
description:
name: flutter_keyboard_visibility_windows
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
url: "https://pub.dev"
source: hosted
version: "1.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -1062,10 +1006,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev"
source: hosted
version: "12.1.3"
version: "14.2.7"
google_fonts:
dependency: "direct main"
description:
@ -1271,14 +1215,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
introduction_screen:
dependency: "direct main"
description:
name: introduction_screen
sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5"
url: "https://pub.dev"
source: hosted
version: "3.1.14"
io:
dependency: "direct dev"
description:
@ -1784,14 +1720,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
retry:
dependency: "direct main"
description:
name: retry
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
riverpod:
dependency: "direct main"
description:

View File

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

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
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64