mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
commit
87a78549b6
6
.github/workflows/spotube-publish-binary.yml
vendored
6
.github/workflows/spotube-publish-binary.yml
vendored
@ -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 }}
|
||||||
|
11
.github/workflows/spotube-release-binary.yml
vendored
11
.github/workflows/spotube-release-binary.yml
vendored
@ -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}}
|
||||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,6 +2,33 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
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
BIN
assets/spotube-logo.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
@ -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
|
||||||
|
104
lib/components/framework/app_pop_scope.dart
Normal file
104
lib/components/framework/app_pop_scope.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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
19
lib/extensions/list.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 () {
|
||||||
|
27
lib/hooks/configurators/use_has_touch.dart
Normal file
27
lib/hooks/configurators/use_has_touch.dart
Normal 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;
|
||||||
|
}
|
@ -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": "المنصة غير مدعومة"
|
||||||
}
|
}
|
@ -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": "সমর্থিত প্ল্যাটফর্ম নয়"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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": "پلتفرم پشتیبانی نمیشود"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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": "असमर्थित प्लेटफार्म"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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": "サポートされていないプラットフォーム"
|
||||||
}
|
}
|
@ -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": "მოუხერხებელი პლატფორმა"
|
||||||
}
|
}
|
@ -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": "지원되지 않는 플랫폼"
|
||||||
}
|
}
|
@ -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": "असमर्थित प्लेटफार्म"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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": "Платформа не поддерживается"
|
||||||
}
|
}
|
@ -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": "แพลตฟอร์มไม่รองรับ"
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
@ -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": "Непідтримувана платформа"
|
||||||
}
|
}
|
@ -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ợ"
|
||||||
}
|
}
|
@ -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": "不支持的平台"
|
||||||
}
|
}
|
@ -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';
|
||||||
@ -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,
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
79
lib/pages/mobile_login/hooks/login_callback.dart
Normal file
79
lib/pages/mobile_login/hooks/login_callback.dart
Normal 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]);
|
||||||
|
}
|
@ -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/"),
|
||||||
|
50
lib/pages/mobile_login/no_webview_runtime_dialog.dart
Normal file
50
lib/pages/mobile_login/no_webview_runtime_dialog.dart
Normal 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',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(() {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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 =
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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(() {
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
78
pubspec.lock
78
pubspec.lock
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user