mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
commit
87a78549b6
8
.github/workflows/spotube-publish-binary.yml
vendored
8
.github/workflows/spotube-publish-binary.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to publish (x.x.x)
|
||||
default: 3.8.0
|
||||
default: 3.8.1
|
||||
required: true
|
||||
dry_run:
|
||||
description: Dry run
|
||||
@ -76,12 +76,12 @@ jobs:
|
||||
commit_message: Updated to v${{ inputs.version }}
|
||||
|
||||
winget:
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(inputs.jobs, 'winget')
|
||||
steps:
|
||||
- name: Release winget package
|
||||
if: ${{ !inputs.dry_run }}
|
||||
uses: vedantmgoyal2009/winget-releaser@v2
|
||||
uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
release-tag: v${{ inputs.version }}
|
||||
@ -134,4 +134,4 @@ jobs:
|
||||
packageName: oss.krtirtho.spotube
|
||||
track: production
|
||||
status: draft
|
||||
releaseName: ${{ env.TAG_NAME }}
|
||||
releaseName: ${{ env.TAG_NAME }}
|
||||
|
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
|
||||
if: ${{matrix.platform == 'linux_arm'}}
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
swap-storage: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
|
||||
- name: Build ${{matrix.platform}} binaries
|
||||
run: dart cli/cli.dart build ${{matrix.platform}}
|
||||
|
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.
|
||||
|
||||
## [3.8.1](https://github.com/krtirtho/spotube/compare/v3.8.0...v3.8.1) (2024-09-15)
|
||||
|
||||
## Changes
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **translations**: correct some basque incorrect translations (#1815)
|
||||
- **lyrics**: LRCLIB lyrics should be usable without logging in #1803
|
||||
- playlist displaying descriptions unescaped html #1784
|
||||
- **android**: pressing back while the player is open doesn't take to previous page
|
||||
- handle dublicated items in playback queue correctly #1852
|
||||
- **desktop**: scrollbar overlapping with more options of tracks and playlists
|
||||
- **discord**: stop discord rpc from try update presence when not connected
|
||||
- **stats**: minutes page shows plays and streams page shows minutes which should be the opposite #1880
|
||||
- **android**: clears queue upon swiping away notification
|
||||
- **player**: shuffle button state resets after closing page #1657
|
||||
- getting started page login page exception #1800
|
||||
- **mobile**: queue doesn't persist
|
||||
- local tracks takes time to load
|
||||
- start radio not working #1629
|
||||
|
||||
### Features
|
||||
|
||||
- **desktop**: show error dialog if webview is not found on login #1871
|
||||
- manually detect and define touch behavior #1763
|
||||
|
||||
|
||||
## [3.8.0](https://github.com/krtirtho/spotube/compare/v3.7.1...v3.8.0) (2024-06-06)
|
||||
|
||||
### Features
|
||||
|
BIN
assets/spotube-logo.bmp
Normal file
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
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
@ -124,7 +122,6 @@ DEPENDENCIES:
|
||||
- flutter_broadcasts (from `.symlinks/plugins/flutter_broadcasts/ios`)
|
||||
- flutter_discord_rpc (from `.symlinks/plugins/flutter_discord_rpc/ios`)
|
||||
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_sharing_intent (from `.symlinks/plugins/flutter_sharing_intent/ios`)
|
||||
@ -173,8 +170,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_discord_rpc/ios"
|
||||
flutter_inappwebview_ios:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
|
||||
flutter_keyboard_visibility:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
@ -220,7 +215,6 @@ SPEC CHECKSUMS:
|
||||
flutter_broadcasts: 3ece15b27d8ccbe2132c3df303e7c3401feab882
|
||||
flutter_discord_rpc: e1c342f29ceb9dd76cdc01db59a70c93bb4d9ec5
|
||||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
flutter_sharing_intent: e35380d0e1501d7111dbb7e46d5ac6339da6da98
|
||||
|
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;
|
||||
|
||||
/// Closes the sliding panel to its collapsed state (i.e. to the minHeight)
|
||||
Future<void> close() {
|
||||
Future<void> close() async {
|
||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||
return _panelState!._close();
|
||||
await _panelState!._close();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Opens the sliding panel fully
|
||||
/// (i.e. to the maxHeight)
|
||||
Future<void> open() {
|
||||
Future<void> open() async {
|
||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||
return _panelState!._open();
|
||||
await _panelState!._open();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Hides the sliding panel (i.e. is invisible)
|
||||
Future<void> hide() {
|
||||
Future<void> hide() async {
|
||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||
return _panelState!._hide();
|
||||
await _panelState!._hide();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Shows the sliding panel in its collapsed state
|
||||
/// (i.e. "un-hide" the sliding panel)
|
||||
Future<void> show() {
|
||||
Future<void> show() async {
|
||||
assert(isAttached, "PanelController must be attached to a SlidingUpPanel");
|
||||
return _panelState!._show();
|
||||
await _panelState!._show();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Animates the panel position to the value.
|
||||
|
@ -58,7 +58,7 @@ class PlaybuttonCard extends HookWidget {
|
||||
others: 15,
|
||||
);
|
||||
|
||||
var unescapeHtml = description?.unescapeHtml();
|
||||
final unescapeHtml = description?.unescapeHtml().cleanHtml();
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: size),
|
||||
margin: margin,
|
||||
|
@ -105,7 +105,9 @@ class TrackOptions extends HookConsumerWidget {
|
||||
final pages =
|
||||
await spotify.search.get(query, types: [SearchType.playlist]).first();
|
||||
|
||||
final radios = pages.map((e) => e.items).toList().cast<PlaylistSimple>();
|
||||
final radios = pages
|
||||
.expand((e) => e.items?.cast<PlaylistSimple>().toList() ?? [])
|
||||
.toList();
|
||||
|
||||
final artists = track.artists!.map((e) => e.name);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -21,6 +22,7 @@ import 'package:spotube/pages/track/track.dart';
|
||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||
import 'package:spotube/provider/audio_player/state.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class TrackTile extends HookConsumerWidget {
|
||||
@ -276,6 +278,7 @@ class TrackTile extends HookConsumerWidget {
|
||||
userPlaylist: userPlaylist,
|
||||
showMenuCbRef: showOptionCbRef,
|
||||
),
|
||||
if (kIsDesktop) const Gap(10),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -15,6 +15,7 @@ import 'package:spotube/components/tracks_view/sections/body/track_view_body_hea
|
||||
import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
|
||||
import 'package:spotube/components/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/components/tracks_view/track_view_provider.dart';
|
||||
import 'package:spotube/extensions/list.dart';
|
||||
import 'package:spotube/models/connect/connect.dart';
|
||||
import 'package:spotube/provider/connect/connect.dart';
|
||||
import 'package:spotube/provider/history/history.dart';
|
||||
@ -65,6 +66,56 @@ class TrackViewBodySection extends HookConsumerWidget {
|
||||
|
||||
final isActive = playlist.collections.contains(props.collectionId);
|
||||
|
||||
final onTapTrackTile = useCallback((Track track, int index) async {
|
||||
if (trackViewState.isSelecting) {
|
||||
trackViewState.toggleTrackSelection(track.id!);
|
||||
return;
|
||||
}
|
||||
|
||||
final isRemoteDevice = await showSelectDeviceDialog(context, ref);
|
||||
|
||||
if (isRemoteDevice) {
|
||||
final remotePlayback = ref.read(connectProvider.notifier);
|
||||
final remoteQueue = ref.read(queueProvider);
|
||||
if (remoteQueue.collections.contains(props.collectionId) ||
|
||||
remoteQueue.tracks.any((s) => s.id == track.id)) {
|
||||
await playlistNotifier.jumpToTrack(track);
|
||||
} else {
|
||||
final tracks = await props.pagination.onFetchAll();
|
||||
await remotePlayback.load(
|
||||
props.collection is AlbumSimple
|
||||
? WebSocketLoadEventData.album(
|
||||
tracks: tracks,
|
||||
collection: props.collection as AlbumSimple,
|
||||
initialIndex: index,
|
||||
)
|
||||
: WebSocketLoadEventData.playlist(
|
||||
tracks: tracks,
|
||||
collection: props.collection as PlaylistSimple,
|
||||
initialIndex: index,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isActive || playlist.tracks.containsBy(track, (a) => a.id)) {
|
||||
await playlistNotifier.jumpToTrack(track);
|
||||
} else {
|
||||
final tracks = await props.pagination.onFetchAll();
|
||||
await playlistNotifier.load(
|
||||
tracks,
|
||||
initialIndex: index,
|
||||
autoPlay: true,
|
||||
);
|
||||
playlistNotifier.addCollection(props.collectionId);
|
||||
if (props.collection is AlbumSimple) {
|
||||
historyNotifier.addAlbums([props.collection as AlbumSimple]);
|
||||
} else {
|
||||
historyNotifier.addPlaylists([props.collection as PlaylistSimple]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isActive, playlist, props, playlistNotifier, historyNotifier]);
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
@ -130,58 +181,7 @@ class TrackViewBodySection extends HookConsumerWidget {
|
||||
trackViewState.selectTrack(track.id!);
|
||||
HapticFeedback.selectionClick();
|
||||
},
|
||||
onTap: () async {
|
||||
if (trackViewState.isSelecting) {
|
||||
trackViewState.toggleTrackSelection(track.id!);
|
||||
return;
|
||||
}
|
||||
|
||||
final isRemoteDevice =
|
||||
await showSelectDeviceDialog(context, ref);
|
||||
|
||||
if (isRemoteDevice) {
|
||||
final remotePlayback = ref.read(connectProvider.notifier);
|
||||
final remoteQueue = ref.read(queueProvider);
|
||||
if (remoteQueue.collections.contains(props.collectionId) ||
|
||||
remoteQueue.tracks.any((s) => s.id == track.id)) {
|
||||
await playlistNotifier.jumpToTrack(track);
|
||||
} else {
|
||||
final tracks = await props.pagination.onFetchAll();
|
||||
await remotePlayback.load(
|
||||
props.collection is AlbumSimple
|
||||
? WebSocketLoadEventData.album(
|
||||
tracks: tracks,
|
||||
collection: props.collection as AlbumSimple,
|
||||
initialIndex: index,
|
||||
)
|
||||
: WebSocketLoadEventData.playlist(
|
||||
tracks: tracks,
|
||||
collection: props.collection as PlaylistSimple,
|
||||
initialIndex: index,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isActive || playlist.tracks.contains(track)) {
|
||||
await playlistNotifier.jumpToTrack(track);
|
||||
} else {
|
||||
final tracks = await props.pagination.onFetchAll();
|
||||
await playlistNotifier.load(
|
||||
tracks,
|
||||
initialIndex: index,
|
||||
autoPlay: true,
|
||||
);
|
||||
playlistNotifier.addCollection(props.collectionId);
|
||||
if (props.collection is AlbumSimple) {
|
||||
historyNotifier
|
||||
.addAlbums([props.collection as AlbumSimple]);
|
||||
} else {
|
||||
historyNotifier
|
||||
.addPlaylists([props.collection as PlaylistSimple]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onTap: () => onTapTrackTile(track, index),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/expandable_search/expandable_search.dart';
|
||||
import 'package:spotube/components/sort_tracks_dropdown.dart';
|
||||
@ -7,6 +8,7 @@ import 'package:spotube/components/tracks_view/track_view_props.dart';
|
||||
import 'package:spotube/components/tracks_view/track_view_provider.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class TrackViewBodyHeaders extends HookConsumerWidget {
|
||||
final ValueNotifier<bool> isFiltering;
|
||||
@ -94,6 +96,7 @@ class TrackViewBodyHeaders extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
const TrackViewBodyOptions(),
|
||||
if (kIsDesktop) const Gap(10),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -128,7 +128,9 @@ class TrackViewFlexHeader extends HookConsumerWidget {
|
||||
if (props.description != null &&
|
||||
props.description!.isNotEmpty)
|
||||
Text(
|
||||
props.description!.unescapeHtml(),
|
||||
props.description!
|
||||
.unescapeHtml()
|
||||
.cleanHtml(),
|
||||
style:
|
||||
defaultTextStyle.style.copyWith(
|
||||
color: palette.bodyTextColor,
|
||||
|
19
lib/extensions/list.dart
Normal file
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/parser.dart';
|
||||
|
||||
final htmlEscape = HtmlUnescape();
|
||||
|
||||
extension UnescapeHtml on String {
|
||||
String cleanHtml() => parse("<p>$this</p>").documentElement!.text;
|
||||
String unescapeHtml() => htmlEscape.convert(this);
|
||||
}
|
||||
|
||||
extension NullableUnescapeHtml on String? {
|
||||
String? cleanHtml() => this?.cleanHtml();
|
||||
String? unescapeHtml() => this?.unescapeHtml();
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
final appLinks = AppLinks();
|
||||
@ -61,30 +62,34 @@ void useDeepLinking(WidgetRef ref) {
|
||||
}
|
||||
|
||||
final subscription = linkStream.listen((uri) async {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
try {
|
||||
final startSegment = uri.split(":").take(2).join(":");
|
||||
final endSegment = uri.split(":").last;
|
||||
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await spotify.albums.get(endSegment),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.push("/artist/$endSegment");
|
||||
break;
|
||||
case "spotify:track":
|
||||
await router.push("/track/$endSegment");
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await spotify.playlists.get(endSegment),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
switch (startSegment) {
|
||||
case "spotify:album":
|
||||
await router.push(
|
||||
"/album/$endSegment",
|
||||
extra: await spotify.albums.get(endSegment),
|
||||
);
|
||||
break;
|
||||
case "spotify:artist":
|
||||
await router.push("/artist/$endSegment");
|
||||
break;
|
||||
case "spotify:track":
|
||||
await router.push("/track/$endSegment");
|
||||
break;
|
||||
case "spotify:playlist":
|
||||
await router.push(
|
||||
"/playlist/$endSegment",
|
||||
extra: await spotify.playlists.get(endSegment),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
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_playlists": "قوائم التشغيل",
|
||||
"summary_were_on_repeat": "كانت على التكرار",
|
||||
"total_money": "المجموع {money}"
|
||||
"total_money": "المجموع {money}",
|
||||
"webview_not_found": "لم يتم العثور على Webview",
|
||||
"webview_not_found_description": "لم يتم تثبيت بيئة تشغيل Webview على جهازك.\nإذا كانت مثبتة، تأكد من وجودها في environment PATH\n\nبعد التثبيت، أعد تشغيل التطبيق",
|
||||
"unsupported_platform": "المنصة غير مدعومة"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "আপনার ভালোবাসা পেয়েছে",
|
||||
"summary_playlists": "প্লেলিস্ট",
|
||||
"summary_were_on_repeat": "পুনরাবৃত্তিতে ছিল",
|
||||
"total_money": "মোট {money}"
|
||||
"total_money": "মোট {money}",
|
||||
"webview_not_found": "ওয়েবভিউ পাওয়া যায়নি",
|
||||
"webview_not_found_description": "আপনার ডিভাইসে কোনো ওয়েবভিউ রানটাইম ইনস্টল করা নেই।\nযদি ইনস্টল থাকে, তা নিশ্চিত করুন যে এটি environment PATH এ রয়েছে\n\nইনস্টল করার পর, অ্যাপটি পুনরায় চালু করুন",
|
||||
"unsupported_platform": "সমর্থিত প্ল্যাটফর্ম নয়"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "ha aconseguit el teu amor",
|
||||
"summary_playlists": "llistes de reproducció",
|
||||
"summary_were_on_repeat": "estaven en repetició",
|
||||
"total_money": "total {money}"
|
||||
"total_money": "total {money}",
|
||||
"webview_not_found": "No s'ha trobat el Webview",
|
||||
"webview_not_found_description": "No hi ha cap temps d'execució de Webview instal·lat al dispositiu.\nSi està instal·lat, assegureu-vos que estigui en el environment PATH\n\nDesprés d'instal·lar-lo, reinicieu l'aplicació",
|
||||
"unsupported_platform": "Plataforma no compatible"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Získal vaši lásku",
|
||||
"summary_playlists": "playlisty",
|
||||
"summary_were_on_repeat": "Byly na opakování",
|
||||
"total_money": "Celkem {money}"
|
||||
"total_money": "Celkem {money}",
|
||||
"webview_not_found": "Webview nebyl nalezen",
|
||||
"webview_not_found_description": "Na vašem zařízení není nainstalováno žádné runtime prostředí Webview.\nPokud je nainstalováno, ujistěte se, že je v environment PATH\n\nPo instalaci restartujte aplikaci",
|
||||
"unsupported_platform": "Nepodporovaná platforma"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Hat Ihre Liebe gewonnen",
|
||||
"summary_playlists": "Wiedergabelisten",
|
||||
"summary_were_on_repeat": "Wurden wiederholt",
|
||||
"total_money": "Gesamt {money}"
|
||||
"total_money": "Gesamt {money}",
|
||||
"webview_not_found": "Webview nicht gefunden",
|
||||
"webview_not_found_description": "Es ist keine Webview-Laufzeitumgebung auf Ihrem Gerät installiert.\nFalls installiert, stellen Sie sicher, dass es im environment PATH ist\n\nNach der Installation starten Sie die App neu",
|
||||
"unsupported_platform": "Nicht unterstützte Plattform"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Got your love",
|
||||
"summary_playlists": "playlists",
|
||||
"summary_were_on_repeat": "Were on repeat",
|
||||
"total_money": "Total {money}"
|
||||
"total_money": "Total {money}",
|
||||
"webview_not_found": "Webview not found",
|
||||
"webview_not_found_description": "No webview runtime is installed in your device.\nIf it's installed make sure it's in the Environment PATH\n\nAfter installing, restart the app",
|
||||
"unsupported_platform": "Unsupported platform"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Obtuvo tu amor",
|
||||
"summary_playlists": "listas de reproducción",
|
||||
"summary_were_on_repeat": "Estaban en repetición",
|
||||
"total_money": "Total {money}"
|
||||
"total_money": "Total {money}",
|
||||
"webview_not_found": "No se encontró el Webview",
|
||||
"webview_not_found_description": "No hay tiempo de ejecución de Webview instalado en su dispositivo.\nSi está instalado, asegúrese de que esté en el environment PATH\n\nDespués de instalar, reinicie la aplicación",
|
||||
"unsupported_platform": "Plataforma no soportada"
|
||||
}
|
@ -367,7 +367,7 @@
|
||||
"count_plays": "{count} erreprodukzio",
|
||||
"streaming_fees_hypothetical": "Streaming ordainketa (hipotetikoa)",
|
||||
"minutes_listened": "Entzundako minutuak",
|
||||
"streamed_songs": "Stream-eatutako kantak",
|
||||
"streamed_songs": "Streaming-ez entzundako kantak",
|
||||
"count_streams": "{count} stream",
|
||||
"owned_by_you": "Zure jabetzakoa",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} arbelera kopiatua",
|
||||
@ -376,13 +376,16 @@
|
||||
"summary_minutes": "minutu",
|
||||
"summary_listened_to_music": "Musika entzuten",
|
||||
"summary_songs": "kanta",
|
||||
"summary_streamed_overall": "Stream-eatuta oro har",
|
||||
"summary_streamed_overall": "Streaming abesti oro har",
|
||||
"summary_owed_to_artists": "Hilabete honetan\nartistei zor zaiena",
|
||||
"summary_artists": "artisten",
|
||||
"summary_music_reached_you": "Musika ailegatu zaizu",
|
||||
"summary_full_albums": "album osok",
|
||||
"summary_got_your_love": "Izan dute zure maitasuna",
|
||||
"summary_got_your_love": "Jaso dute zure maitasuna",
|
||||
"summary_playlists": "zerrenda",
|
||||
"summary_were_on_repeat": "Dituzu errepikatze moduan",
|
||||
"total_money": "Guztira {money}"
|
||||
"total_money": "Guztira {money}",
|
||||
"webview_not_found": "Ez da Webview aurkitu",
|
||||
"webview_not_found_description": "Ez dago Webview abiarazte denbora-instalaziorik zure gailuan.\nInstalatuta badago, ziurtatu environment PATH-an dagoela\n\nInstalatu ondoren, berrabiarazi aplikazioa",
|
||||
"unsupported_platform": "Plataforma ez onartua"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "عشق شما را به دست آورد",
|
||||
"summary_playlists": "لیستهای پخش",
|
||||
"summary_were_on_repeat": "در تکرار بودند",
|
||||
"total_money": "مجموع {money}"
|
||||
"total_money": "مجموع {money}",
|
||||
"webview_not_found": "وبویو پیدا نشد",
|
||||
"webview_not_found_description": "هیچ اجرای وبویو روی دستگاه شما نصب نشده است.\nدر صورت نصب، مطمئن شوید که در environment PATH قرار دارد\n\nپس از نصب، برنامه را مجدداً راهاندازی کنید",
|
||||
"unsupported_platform": "پلتفرم پشتیبانی نمیشود"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Sai rakkautesi",
|
||||
"summary_playlists": "soittolistat",
|
||||
"summary_were_on_repeat": "Olivat toistossa",
|
||||
"total_money": "Yhteensä {money}"
|
||||
"total_money": "Yhteensä {money}",
|
||||
"webview_not_found": "Webview ei löydy",
|
||||
"webview_not_found_description": "Laitteellasi ei ole asennettua Webview-ajonaikaa.\nJos se on asennettu, varmista, että se on environment PATH:ssa\n\nAsennuksen jälkeen käynnistä sovellus uudelleen",
|
||||
"unsupported_platform": "Ei tuettu alusta"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "A obtenu votre amour",
|
||||
"summary_playlists": "playlists",
|
||||
"summary_were_on_repeat": "Était en répétition",
|
||||
"total_money": "Total {money}"
|
||||
"total_money": "Total {money}",
|
||||
"webview_not_found": "Webview non trouvé",
|
||||
"webview_not_found_description": "Aucun environnement d'exécution Webview installé sur votre appareil.\nSi c'est installé, assurez-vous qu'il soit dans le environment PATH\n\nAprès l'installation, redémarrez l'application",
|
||||
"unsupported_platform": "Plateforme non prise en charge"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} स्ट्रिम",
|
||||
"owned_by_you": "तपाईंले स्वामित्व गरेको",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो",
|
||||
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।"
|
||||
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
|
||||
"webview_not_found": "वेबव्यू नहीं मिला",
|
||||
"webview_not_found_description": "आपके डिवाइस पर वेबव्यू रनटाइम इंस्टॉल नहीं है।\nअगर इंस्टॉल है, तो सुनिश्चित करें कि यह environment PATH में है\n\nइंस्टॉल करने के बाद, ऐप को पुनः शुरू करें",
|
||||
"unsupported_platform": "असमर्थित प्लेटफार्म"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"summary_got_your_love": "Mendapatkan cinta Anda",
|
||||
"summary_playlists": "daftar putar",
|
||||
"summary_were_on_repeat": "Sedang diulang",
|
||||
"total_money": "Total {money}"
|
||||
"total_money": "Total {money}",
|
||||
"webview_not_found": "Webview tidak ditemukan",
|
||||
"webview_not_found_description": "Tidak ada runtime Webview yang diinstal di perangkat Anda.\nJika sudah diinstal, pastikan itu ada di environment PATH\n\nSetelah diinstal, restart aplikasi",
|
||||
"unsupported_platform": "Platform tidak didukung"
|
||||
}
|
@ -385,5 +385,8 @@
|
||||
"summary_got_your_love": "Ha ricevuto il tuo amore",
|
||||
"summary_playlists": "playlist",
|
||||
"summary_were_on_repeat": "Erano in ripetizione",
|
||||
"total_money": "Totale {money}"
|
||||
"total_money": "Totale {money}",
|
||||
"webview_not_found": "Webview non trovato",
|
||||
"webview_not_found_description": "Nessun runtime Webview installato nel tuo dispositivo.\nSe è installato, assicurati che sia nel environment PATH\n\nDopo l'installazione, riavvia l'app",
|
||||
"unsupported_platform": "Piattaforma non supportata"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} 回のストリーム",
|
||||
"owned_by_you": "あなたが所有",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} をクリップボードにコピーしました",
|
||||
"spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。"
|
||||
"spotify_hipotetical_calculation": "*これは、Spotifyのストリームごとの支払い\nが $0.003 から $0.005 の範囲で計算されています。これは仮想的な\n計算で、Spotify で曲を聴いた場合に、アーティストに\nどれくらい支払ったかをユーザーに示すためのものです。",
|
||||
"webview_not_found": "Webviewが見つかりません",
|
||||
"webview_not_found_description": "デバイスにWebviewランタイムがインストールされていません。\nインストールされている場合は、environment PATHにあることを確認してください\n\nインストール後、アプリを再起動してください",
|
||||
"unsupported_platform": "サポートされていないプラットフォーム"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} სტრიმი",
|
||||
"owned_by_you": "შენ მიერ საკუთრებული",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} აიღო კლიპბორდზე",
|
||||
"spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე."
|
||||
"spotify_hipotetical_calculation": "*ეს გამოითვლება Spotify-ის თითოეულ სტრიმზე\nგადახდის შესაბამისად, რომელიც $0.003 დან $0.005-მდეა. ეს არის ჰიპოთეტური\nგამოთვლა, რომელიც აჩვენებს მომხმარებელს რამდენი გადაიხდიდა\nარტისტებს, თუკი ისინი უსმენდნენ მათ სიმღერებს Spotify-ზე.",
|
||||
"webview_not_found": "ვებვიუ ვერ მოიძებნა",
|
||||
"webview_not_found_description": "თქვენს მოწყობილობაზე ვებვიუის შესრულების დრო არ არის დაყენებული.\nთუ დაყენებულია, დარწმუნდით, რომ ის environment PATH-შია\n\nდაყენების შემდეგ, გადატვირთეთ აპი",
|
||||
"unsupported_platform": "მოუხერხებელი პლატფორმა"
|
||||
}
|
@ -385,5 +385,8 @@
|
||||
"count_streams": "{count} 스트림",
|
||||
"owned_by_you": "당신이 소유",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl}를 클립보드에 복사했습니다",
|
||||
"spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다."
|
||||
"spotify_hipotetical_calculation": "*Spotify의 스트림당 지불금 $0.003에서 $0.005까지의\n기준으로 계산되었습니다. 이는 사용자가 Spotify에서\n곡을 들을 때 아티스트에게 얼마를 지불했을지를\n알려주기 위한 가상의 계산입니다.",
|
||||
"webview_not_found": "웹뷰를 찾을 수 없음",
|
||||
"webview_not_found_description": "기기에 웹뷰 런타임이 설치되지 않았습니다.\n설치되어 있으면 environment PATH에 있는지 확인하십시오\n\n설치 후 앱을 다시 시작하세요",
|
||||
"unsupported_platform": "지원되지 않는 플랫폼"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} स्ट्रिम",
|
||||
"owned_by_you": "तपाईंले स्वामित्व गरेको",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} क्लिपबोर्डमा कपी गरियो",
|
||||
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।"
|
||||
"spotify_hipotetical_calculation": "*यो Spotify को प्रति स्ट्रीम भुगतानको आधारमा\n$0.003 देखि $0.005 को बीचमा गणना गरिएको हो। यो एक काल्पनिक\nगणना हो जसले प्रयोगकर्तालाई देखाउँछ कि उनीहरूले कति\nअर्टिस्टहरूलाई तिनीहरूका गीतहरू Spotify मा सुनेमा\nभुक्तान गर्नुपर्ने थियो।",
|
||||
"webview_not_found": "वेबभ्यू फेला परेन",
|
||||
"webview_not_found_description": "तपाईंको उपकरणमा कुनै वेबभ्यू रनटाइम स्थापना गरिएको छैन।\nयदि स्थापना गरिएको छ भने, environment PATH मा छ कि छैन भनेर सुनिश्चित गर्नुहोस्\n\nस्थापना पछि, अनुप्रयोग पुनः सुरु गर्नुहोस्",
|
||||
"unsupported_platform": "असमर्थित प्लेटफार्म"
|
||||
}
|
@ -385,5 +385,8 @@
|
||||
"count_streams": "{count} streams",
|
||||
"owned_by_you": "Bezit door jou",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} gekopieerd naar klembord",
|
||||
"spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren."
|
||||
"spotify_hipotetical_calculation": "*Dit is berekend op basis van Spotify's betaling per stream\nvan $0.003 tot $0.005. Dit is een hypothetische\nberekening om de gebruiker inzicht te geven in hoeveel ze\naan de artiesten zouden hebben betaald als ze hun liedjes op Spotify\nzouden luisteren.",
|
||||
"webview_not_found": "Webview niet gevonden",
|
||||
"webview_not_found_description": "Er is geen Webview-runtime geïnstalleerd op uw apparaat.\nAls het is geïnstalleerd, zorg ervoor dat het in het environment PATH staat\n\nHerstart de app na installatie",
|
||||
"unsupported_platform": "Niet ondersteund platform"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} strumieni",
|
||||
"owned_by_you": "Własność Twoja",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} skopiowano do schowka",
|
||||
"spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify."
|
||||
"spotify_hipotetical_calculation": "*Obliczone na podstawie płatności Spotify za strumień\nw zakresie od $0.003 do $0.005. Jest to hipotetyczne\nobliczenie mające na celu pokazanie użytkownikowi, ile\nzapłaciliby artystom, gdyby słuchali ich utworów na Spotify.",
|
||||
"webview_not_found": "Nie znaleziono Webview",
|
||||
"webview_not_found_description": "Na twoim urządzeniu nie zainstalowano środowiska uruchomieniowego Webview.\nJeśli jest zainstalowany, upewnij się, że jest w environment PATH\n\nPo instalacji uruchom ponownie aplikację",
|
||||
"unsupported_platform": "Nieobsługiwana platforma"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} streams",
|
||||
"owned_by_you": "De sua propriedade",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} copiado para a área de transferência",
|
||||
"spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify."
|
||||
"spotify_hipotetical_calculation": "*Isso é calculado com base no pagamento por stream do Spotify\nque varia de $0.003 a $0.005. Esta é uma cálculo hipotético\npara dar ao usuário uma visão de quanto teriam pago aos artistas\nse eles ouvissem suas músicas no Spotify.",
|
||||
"webview_not_found": "Webview não encontrado",
|
||||
"webview_not_found_description": "Nenhum runtime Webview está instalado no seu dispositivo.\nSe estiver instalado, certifique-se de que está no environment PATH\n\nApós a instalação, reinicie o aplicativo",
|
||||
"unsupported_platform": "Plataforma não suportada"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} стримов",
|
||||
"owned_by_you": "Ваша собственность",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} скопировано в буфер обмена",
|
||||
"spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify."
|
||||
"spotify_hipotetical_calculation": "*Это рассчитано на основе выплат Spotify за стрим\nот $0.003 до $0.005. Это гипотетический расчет,\nчтобы дать пользователю представление о том, сколько бы он\nзаплатил артистам, если бы слушал их песни на Spotify.",
|
||||
"webview_not_found": "Webview не найден",
|
||||
"webview_not_found_description": "На вашем устройстве не установлена среда выполнения Webview.\nЕсли он установлен, убедитесь, что он находится в environment PATH\n\nПосле установки перезапустите приложение",
|
||||
"unsupported_platform": "Платформа не поддерживается"
|
||||
}
|
@ -385,5 +385,8 @@
|
||||
"count_streams": "{count} สตรีม",
|
||||
"owned_by_you": "เป็นเจ้าของโดยคุณ",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} คัดลอกไปที่คลิปบอร์ดแล้ว",
|
||||
"spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify."
|
||||
"spotify_hipotetical_calculation": "*คำนวณตามการจ่ายต่อสตรีมของ Spotify\nซึ่งอยู่ในช่วง $0.003 ถึง $0.005 นี่เป็นการคำนวณสมมุติ\nเพื่อให้ผู้ใช้ทราบว่าพวกเขาจะจ่ายเงินให้ศิลปินเท่าไหร่\nหากพวกเขาฟังเพลงของพวกเขาใน Spotify.",
|
||||
"webview_not_found": "ไม่พบ Webview",
|
||||
"webview_not_found_description": "ไม่พบ runtime ของ Webview บนอุปกรณ์ของคุณ\nหากติดตั้งแล้วตรวจสอบให้แน่ใจว่าอยู่ใน environment PATH\n\nหลังจากติดตั้งแล้ว ให้รีสตาร์ทแอป",
|
||||
"unsupported_platform": "แพลตฟอร์มไม่รองรับ"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} yayın",
|
||||
"owned_by_you": "Sahip olduğunuz",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} panoya kopyalandı",
|
||||
"spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir."
|
||||
"spotify_hipotetical_calculation": "*Bu, Spotify'ın her yayın başına ödemenin\n$0.003 ile $0.005 arasında olduğu varsayımıyla hesaplanmıştır. Bu\nhipotetik bir hesaplamadır, kullanıcıya şarkılarını Spotify'da dinlediklerinde\nsanatçılara ne kadar ödeme yapacaklarını gösterir.",
|
||||
"webview_not_found": "Webview bulunamadı",
|
||||
"webview_not_found_description": "Cihazınızda herhangi bir Webview çalışma zamanı yüklü değil.\nEğer kuruluysa, ortam YOLUNDA olduğundan emin olun\n\nKurulumdan sonra uygulamayı yeniden başlatın",
|
||||
"unsupported_platform": "Desteklenmeyen platform"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} стримів",
|
||||
"owned_by_you": "Ваша власність",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} скопійовано в буфер обміну",
|
||||
"spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify."
|
||||
"spotify_hipotetical_calculation": "*Це розраховано на основі виплат Spotify за стрім\nвід $0.003 до $0.005. Це гіпотетичний розрахунок,\nщоб дати користувачеві уявлення про те, скільки б він заплатив\nартистам, якби слухав їхні пісні на Spotify.",
|
||||
"webview_not_found": "Webview не знайдено",
|
||||
"webview_not_found_description": "На вашому пристрої не встановлено виконуване середовище Webview.\nЯкщо воно встановлено, переконайтеся, що воно знаходиться в environment PATH\n\nПісля встановлення перезапустіть програму",
|
||||
"unsupported_platform": "Непідтримувана платформа"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} lượt phát",
|
||||
"owned_by_you": "Thuộc sở hữu của bạn",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} đã sao chép vào bảng tạm",
|
||||
"spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify."
|
||||
"spotify_hipotetical_calculation": "*Được tính toán dựa trên khoản thanh toán của Spotify cho mỗi lượt phát\ntừ $0.003 đến $0.005. Đây là một tính toán giả định để\ncung cấp cho người dùng cái nhìn về số tiền họ sẽ phải trả\ncho các nghệ sĩ nếu họ nghe bài hát của họ trên Spotify.",
|
||||
"webview_not_found": "Không tìm thấy Webview",
|
||||
"webview_not_found_description": "Không có runtime Webview nào được cài đặt trên thiết bị của bạn.\nNếu đã cài đặt, hãy đảm bảo rằng nó nằm trong environment PATH\n\nSau khi cài đặt, hãy khởi động lại ứng dụng",
|
||||
"unsupported_platform": "Nền tảng không được hỗ trợ"
|
||||
}
|
@ -384,5 +384,8 @@
|
||||
"count_streams": "{count} 次流媒体",
|
||||
"owned_by_you": "由您拥有",
|
||||
"copied_shareurl_to_clipboard": "{shareUrl} 已复制到剪贴板",
|
||||
"spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算,用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。"
|
||||
"spotify_hipotetical_calculation": "*根据 Spotify 每次流媒体的支付金额\n$0.003 到 $0.005 进行计算。这是一个假设性的\n计算,用于给用户了解他们如果在 Spotify 上\n收听歌曲会支付给艺术家的金额。",
|
||||
"webview_not_found": "未找到 Webview",
|
||||
"webview_not_found_description": "您的设备中未安装 Webview 运行时。\n如果已安装,请确保它在 environment PATH 中\n\n安装后,重新启动应用程序",
|
||||
"unsupported_platform": "不支持的平台"
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -22,6 +23,7 @@ import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
||||
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
||||
import 'package:spotube/hooks/configurators/use_fix_window_stretching.dart';
|
||||
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
||||
import 'package:spotube/hooks/configurators/use_has_touch.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
@ -92,7 +94,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
await FlutterDiscordRPC.initialize(Env.discordAppId);
|
||||
}
|
||||
|
||||
if(kIsWindows){
|
||||
if (kIsWindows) {
|
||||
await SMTCWindows.initialize();
|
||||
}
|
||||
|
||||
@ -142,6 +144,7 @@ class Spotube extends HookConsumerWidget {
|
||||
final paletteColor =
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
final router = ref.watch(routerProvider);
|
||||
final hasTouchSupport = useHasTouch();
|
||||
|
||||
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
|
||||
ref.listen(bonsoirProvider, (_, __) {});
|
||||
@ -191,8 +194,22 @@ class Spotube extends HookConsumerWidget {
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Spotube',
|
||||
builder: (context, child) {
|
||||
if (kIsDesktop && !kIsMacOS) return DragToResizeArea(child: child!);
|
||||
return child!;
|
||||
child = ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: hasTouchSupport
|
||||
? {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
|
||||
if (kIsDesktop && !kIsMacOS) child = DragToResizeArea(child: child);
|
||||
|
||||
return child;
|
||||
},
|
||||
themeMode: themeMode,
|
||||
theme: lightTheme,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
int useSyncedLyrics(
|
||||
WidgetRef ref,
|
||||
@ -13,8 +14,12 @@ int useSyncedLyrics(
|
||||
|
||||
useEffect(() {
|
||||
return stream.listen((pos) {
|
||||
if (lyricsMap.containsKey(pos.inSeconds + delay)) {
|
||||
currentTime.value = pos.inSeconds + delay;
|
||||
try {
|
||||
if (lyricsMap.containsKey(pos.inSeconds + delay)) {
|
||||
currentTime.value = pos.inSeconds + delay;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}).cancel;
|
||||
}, [lyricsMap, delay]);
|
||||
|
@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||
import 'package:spotube/modules/player/player_actions.dart';
|
||||
import 'package:spotube/modules/player/player_controls.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
@ -100,11 +101,10 @@ class PlayerView extends HookConsumerWidget {
|
||||
|
||||
final topPadding = MediaQueryData.fromView(View.of(context)).padding.top;
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return AppPopScope(
|
||||
canPop: context.canPop(),
|
||||
onPopInvoked: (didPop) async {
|
||||
await panelController.close();
|
||||
return false;
|
||||
},
|
||||
child: IconTheme(
|
||||
data: theme.iconTheme.copyWith(color: bodyTextColor),
|
||||
|
@ -170,27 +170,26 @@ class PlayerControls extends HookConsumerWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
StreamBuilder<bool>(
|
||||
stream: audioPlayer.shuffledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffled = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
tooltip: shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||
onPressed: isFetchingActiveTrack
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
Consumer(builder: (context, ref, _) {
|
||||
final shuffled = ref
|
||||
.watch(audioPlayerProvider.select((s) => s.shuffled));
|
||||
return IconButton(
|
||||
tooltip: shuffled
|
||||
? context.l10n.unshuffle_playlist
|
||||
: context.l10n.shuffle_playlist,
|
||||
icon: const Icon(SpotubeIcons.shuffle),
|
||||
style: shuffled ? activeButtonStyle : buttonStyle,
|
||||
onPressed: isFetchingActiveTrack
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
tooltip: context.l10n.previous_track,
|
||||
icon: const Icon(SpotubeIcons.skipBack),
|
||||
|
@ -17,7 +17,6 @@ import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/hooks/utils/use_brightness_value.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
@ -30,7 +29,6 @@ class BottomPlayer extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final layoutMode =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));
|
||||
@ -89,35 +87,34 @@ class BottomPlayer extends HookConsumerWidget {
|
||||
children: [
|
||||
PlayerActions(
|
||||
extraActions: [
|
||||
if (auth.asData?.value != null)
|
||||
IconButton(
|
||||
tooltip: context.l10n.mini_player,
|
||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||
onPressed: () async {
|
||||
if (!kIsDesktop) return;
|
||||
IconButton(
|
||||
tooltip: context.l10n.mini_player,
|
||||
icon: const Icon(SpotubeIcons.miniPlayer),
|
||||
onPressed: () async {
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
final prevSize = await windowManager.getSize();
|
||||
await windowManager.setMinimumSize(
|
||||
const Size(300, 300),
|
||||
);
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
if (!kIsLinux) {
|
||||
await windowManager.setHasShadow(false);
|
||||
}
|
||||
await windowManager
|
||||
.setAlignment(Alignment.topRight);
|
||||
await windowManager.setSize(const Size(400, 500));
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() async {
|
||||
GoRouter.of(context).go(
|
||||
'/mini-player',
|
||||
extra: prevSize,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
final prevSize = await windowManager.getSize();
|
||||
await windowManager.setMinimumSize(
|
||||
const Size(300, 300),
|
||||
);
|
||||
await windowManager.setAlwaysOnTop(true);
|
||||
if (!kIsLinux) {
|
||||
await windowManager.setHasShadow(false);
|
||||
}
|
||||
await windowManager
|
||||
.setAlignment(Alignment.topRight);
|
||||
await windowManager.setSize(const Size(400, 500));
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
() async {
|
||||
GoRouter.of(context).go(
|
||||
'/mini-player',
|
||||
extra: prevSize,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
|
@ -6,7 +6,7 @@ import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/getting_started/blur_card.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/home/home.dart';
|
||||
import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
||||
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@ -16,6 +16,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
|
||||
final onLogin = useLoginCallback(ref);
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
@ -121,9 +122,7 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
|
||||
),
|
||||
onPressed: () async {
|
||||
await KVStoreService.setDoneGettingStarted(true);
|
||||
if (context.mounted) {
|
||||
context.pushNamed(WebViewLogin.name);
|
||||
}
|
||||
await onLogin();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -6,7 +6,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/components/themed_button_tab_bar.dart';
|
||||
@ -17,7 +16,6 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
|
||||
import 'package:spotube/hooks/utils/use_palette_color.dart';
|
||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
@ -82,15 +80,6 @@ class LyricsPage extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
|
||||
if (auth.asData?.value == null) {
|
||||
return Scaffold(
|
||||
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
|
||||
body: const AnonymousFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
if (isModal) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
|
@ -8,13 +8,10 @@ import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/player/player_controls.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
import 'package:spotube/modules/root/sidebar.dart';
|
||||
import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/utils/use_force_update.dart';
|
||||
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
|
||||
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@ -46,14 +43,7 @@ class MiniLyricsPage extends HookConsumerWidget {
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
|
||||
if (auth.asData?.value == null) {
|
||||
return const Scaffold(
|
||||
appBar: PageWindowTitleBar(),
|
||||
body: AnonymousFallback(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: !hoverMode.value
|
||||
|
@ -17,6 +17,7 @@ import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
import 'package:stroke_text/stroke_text.dart';
|
||||
|
||||
@ -80,12 +81,16 @@ class SyncedLyrics extends HookConsumerWidget {
|
||||
StreamSubscription? subscription;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
subscription = audioPlayer.positionStream.listen((event) {
|
||||
if (event > Duration.zero) return;
|
||||
controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
try {
|
||||
if (event > Duration.zero || !controller.hasClients) return;
|
||||
controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
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(
|
||||
initialSettings: InAppWebViewSettings(
|
||||
userAgent:
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 afari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 safari/537.36",
|
||||
),
|
||||
initialUrlRequest: URLRequest(
|
||||
url: WebUri("https://accounts.spotify.com/"),
|
||||
|
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:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/collections/side_bar_tiles.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/components/framework/app_pop_scope.dart';
|
||||
import 'package:spotube/modules/player/player_queue.dart';
|
||||
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
|
||||
import 'package:spotube/modules/root/bottom_player.dart';
|
||||
@ -30,10 +32,11 @@ class RootApp extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final showingDialogCompleter = useRef(Completer()..complete());
|
||||
final downloader = ref.watch(downloadManagerProvider);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||
|
||||
useEffect(() {
|
||||
@ -164,55 +167,69 @@ class RootApp extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [backgroundColor]);
|
||||
|
||||
// ignore: deprecated_member_use
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
final routerState = GoRouterState.of(context);
|
||||
if (routerState.matchedLocation != "/") {
|
||||
context.goNamed(HomePage.name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Sidebar(child: child),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: kIsDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final playlistNotifier =
|
||||
ref.read(audioPlayerProvider.notifier);
|
||||
final navTileNames = useMemoized(() {
|
||||
return getSidebarTileList(context.l10n).map((s) => s.name).toList();
|
||||
}, []);
|
||||
|
||||
return PlayerQueue.fromAudioPlayerNotifier(
|
||||
floating: true,
|
||||
playlist: playlist,
|
||||
notifier: playlistNotifier,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(),
|
||||
],
|
||||
),
|
||||
final scaffold = Scaffold(
|
||||
body: Sidebar(child: child),
|
||||
extendBody: true,
|
||||
drawerScrimColor: Colors.transparent,
|
||||
endDrawer: kIsDesktop
|
||||
? Container(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: theme.brightness == Brightness.light
|
||||
? null
|
||||
: kElevationToShadow[8],
|
||||
),
|
||||
margin: const EdgeInsets.only(
|
||||
top: 40,
|
||||
bottom: 100,
|
||||
),
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final playlistNotifier =
|
||||
ref.read(audioPlayerProvider.notifier);
|
||||
|
||||
return PlayerQueue.fromAudioPlayerNotifier(
|
||||
floating: true,
|
||||
playlist: playlist,
|
||||
notifier: playlistNotifier,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomPlayer(),
|
||||
SpotubeNavigationBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (!kIsAndroid) {
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
final topRoute = GoRouterState.of(context).topRoute;
|
||||
final canPop = topRoute != null && !navTileNames.contains(topRoute.name);
|
||||
|
||||
return AppPopScope(
|
||||
canPop: canPop,
|
||||
onPopInvoked: (didPop) {
|
||||
if (didPop) return;
|
||||
|
||||
if (topRoute?.name == HomePage.name) {
|
||||
SystemNavigator.pop();
|
||||
} else {
|
||||
context.goNamed(HomePage.name);
|
||||
}
|
||||
},
|
||||
child: scaffold,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
|
||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/provider/logs/logs_provider.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class LogsPage extends HookConsumerWidget {
|
||||
static const name = "logs";
|
||||
@ -40,6 +41,17 @@ class LogsPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(SpotubeIcons.trash),
|
||||
iconSize: 16,
|
||||
onPressed: () async {
|
||||
ref.invalidate(logsProvider);
|
||||
|
||||
final logsFile = await AppLogger.getLogsPath();
|
||||
|
||||
await logsFile.writeAsString("");
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
|
@ -1,24 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||
import 'package:spotube/components/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/pages/mobile_login/mobile_login.dart';
|
||||
import 'package:spotube/pages/profile/profile.dart';
|
||||
import 'package:spotube/pages/mobile_login/hooks/login_callback.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
class SettingsAccountSection extends HookConsumerWidget {
|
||||
@ -30,7 +24,6 @@ class SettingsAccountSection extends HookConsumerWidget {
|
||||
final router = GoRouter.of(context);
|
||||
|
||||
final auth = ref.watch(authenticationProvider);
|
||||
final authNotifier = ref.watch(authenticationProvider.notifier);
|
||||
final scrobbler = ref.watch(scrobblerProvider);
|
||||
final me = ref.watch(meProvider);
|
||||
final meData = me.asData?.value;
|
||||
@ -40,51 +33,7 @@ class SettingsAccountSection extends HookConsumerWidget {
|
||||
foregroundColor: Colors.white,
|
||||
);
|
||||
|
||||
void onLogin() async {
|
||||
if (kIsMobile) {
|
||||
router.pushNamed(WebViewLogin.name);
|
||||
return;
|
||||
}
|
||||
|
||||
final exp = RegExp(r"https:\/\/accounts.spotify.com\/.+\/status");
|
||||
final applicationSupportDir = await getApplicationSupportDirectory();
|
||||
final userDataFolder = Directory(
|
||||
join(applicationSupportDir.path, "webview_window_Webview2"));
|
||||
|
||||
if (!await userDataFolder.exists()) {
|
||||
await userDataFolder.create();
|
||||
}
|
||||
|
||||
final webview = await WebviewWindow.create(
|
||||
configuration: CreateConfiguration(
|
||||
title: "Spotify Login",
|
||||
titleBarTopPadding: kIsMacOS ? 20 : 0,
|
||||
windowHeight: 720,
|
||||
windowWidth: 1280,
|
||||
userDataFolderWindows: userDataFolder.path,
|
||||
),
|
||||
);
|
||||
webview
|
||||
..setBrightness(theme.colorScheme.brightness)
|
||||
..launch("https://accounts.spotify.com/")
|
||||
..setOnUrlRequestCallback((url) {
|
||||
if (exp.hasMatch(url)) {
|
||||
webview.getAllCookies().then((cookies) async {
|
||||
final cookieHeader =
|
||||
"sp_dc=${cookies.firstWhere((element) => element.name.contains("sp_dc")).value.replaceAll("\u0000", "")}";
|
||||
|
||||
await authNotifier.login(cookieHeader);
|
||||
|
||||
webview.close();
|
||||
if (context.mounted) {
|
||||
context.go("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
final onLogin = useLoginCallback(ref);
|
||||
|
||||
return SectionCardWithHeading(
|
||||
heading: context.l10n.account,
|
||||
|
@ -49,8 +49,8 @@ class StatsMinutesPage extends HookConsumerWidget {
|
||||
return StatsTrackItem(
|
||||
track: track.track,
|
||||
info: Text(
|
||||
context.l10n
|
||||
.count_plays(compactNumberFormatter.format(track.count)),
|
||||
context.l10n.count_mins(compactNumberFormatter
|
||||
.format(track.count * track.track.duration!.inMinutes)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -49,8 +49,8 @@ class StatsStreamsPage extends HookConsumerWidget {
|
||||
return StatsTrackItem(
|
||||
track: track.track,
|
||||
info: Text(
|
||||
context.l10n.count_mins(compactNumberFormatter
|
||||
.format(track.count * track.track.duration!.inMinutes)),
|
||||
context.l10n
|
||||
.count_plays(compactNumberFormatter.format(track.count)),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -14,6 +14,7 @@ import 'package:spotube/components/titlebar/titlebar.dart';
|
||||
import 'package:spotube/components/track_tile/track_options.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/extensions/list.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
@ -167,7 +168,8 @@ class TrackPage extends HookConsumerWidget {
|
||||
children: [
|
||||
const Gap(5),
|
||||
if (!isActive &&
|
||||
!playlist.tracks.contains(track))
|
||||
!playlist.tracks
|
||||
.containsBy(track, (t) => t.id))
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
label: Text(context.l10n.queue),
|
||||
@ -177,7 +179,8 @@ class TrackPage extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(5),
|
||||
if (!isActive &&
|
||||
!playlist.tracks.contains(track))
|
||||
!playlist.tracks
|
||||
.containsBy(track, (t) => t.id))
|
||||
IconButton.outlined(
|
||||
icon:
|
||||
const Icon(SpotubeIcons.lightning),
|
||||
|
@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:spotify/spotify.dart' hide Playlist;
|
||||
import 'package:spotube/extensions/list.dart';
|
||||
import 'package:spotube/extensions/track.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/discord_provider.dart';
|
||||
import 'package:spotube/provider/server/sourced_track.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
BlackListNotifier get _blacklist => ref.read(blacklistProvider.notifier);
|
||||
@ -141,36 +143,52 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
build() {
|
||||
final subscriptions = [
|
||||
audioPlayer.playingStream.listen((playing) async {
|
||||
state = state.copyWith(playing: playing);
|
||||
try {
|
||||
state = state.copyWith(playing: playing);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
playing: Value(playing),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
playing: Value(playing),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.loopModeStream.listen((loopMode) async {
|
||||
state = state.copyWith(loopMode: loopMode);
|
||||
try {
|
||||
state = state.copyWith(loopMode: loopMode);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
loopMode: Value(loopMode),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
loopMode: Value(loopMode),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.shuffledStream.listen((shuffled) async {
|
||||
state = state.copyWith(shuffled: shuffled);
|
||||
try {
|
||||
state = state.copyWith(shuffled: shuffled);
|
||||
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
shuffled: Value(shuffled),
|
||||
),
|
||||
);
|
||||
await _updatePlayerState(
|
||||
AudioPlayerStateTableCompanion(
|
||||
shuffled: Value(shuffled),
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.playlistStream.listen((playlist) async {
|
||||
state = state.copyWith(playlist: playlist);
|
||||
try {
|
||||
state = state.copyWith(playlist: playlist);
|
||||
|
||||
await _updatePlaylist(playlist);
|
||||
await _updatePlaylist(playlist);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@ -239,6 +257,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
final track = tracks.elementAt(i);
|
||||
|
||||
if (state.tracks.any((element) => _compareTracks(element, track))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await audioPlayer.addTrackAt(
|
||||
SpotubeMedia(track),
|
||||
max(state.playlist.index, 0) + i + 1,
|
||||
@ -248,6 +270,7 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
|
||||
Future<void> addTrack(Track track) async {
|
||||
if (_blacklist.contains(track)) return;
|
||||
if (state.tracks.any((element) => _compareTracks(element, track))) return;
|
||||
await audioPlayer.addTrack(SpotubeMedia(track));
|
||||
}
|
||||
|
||||
@ -272,13 +295,23 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _compareTracks(Track a, Track b) {
|
||||
if ((a is LocalTrack && b is! LocalTrack) ||
|
||||
(a is! LocalTrack && b is LocalTrack)) return false;
|
||||
|
||||
return a is LocalTrack && b is LocalTrack
|
||||
? (a).path == (b).path
|
||||
: a.id == b.id;
|
||||
}
|
||||
|
||||
Future<void> load(
|
||||
List<Track> tracks, {
|
||||
int initialIndex = 0,
|
||||
bool autoPlay = false,
|
||||
}) async {
|
||||
final medias =
|
||||
(_blacklist.filter(tracks).toList() as List<Track>).asMediaList();
|
||||
final medias = (_blacklist.filter(tracks).toList() as List<Track>)
|
||||
.asMediaList()
|
||||
.unique((a, b) => _compareTracks(a.track, b.track));
|
||||
|
||||
// Giving the initial track a boost so MediaKit won't skip
|
||||
// because of timeout
|
||||
|
@ -73,25 +73,33 @@ class AudioPlayerStreamListeners {
|
||||
|
||||
StreamSubscription subscribeToPlaylist() {
|
||||
return audioPlayer.playlistStream.listen((mpvPlaylist) {
|
||||
notificationService.addTrack(audioPlayerState.activeTrack!);
|
||||
discord.updatePresence(audioPlayerState.activeTrack!);
|
||||
updatePalette();
|
||||
try {
|
||||
notificationService.addTrack(audioPlayerState.activeTrack!);
|
||||
discord.updatePresence(audioPlayerState.activeTrack!);
|
||||
updatePalette();
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription subscribeToSkipSponsor() {
|
||||
return audioPlayer.positionStream.listen((position) async {
|
||||
final currentSegments = await ref.read(segmentProvider.future);
|
||||
try {
|
||||
final currentSegments = await ref.read(segmentProvider.future);
|
||||
|
||||
if (currentSegments?.segments.isNotEmpty != true ||
|
||||
position < const Duration(seconds: 3)) return;
|
||||
if (currentSegments?.segments.isNotEmpty != true ||
|
||||
position < const Duration(seconds: 3)) return;
|
||||
|
||||
for (final segment in currentSegments!.segments) {
|
||||
final seconds = position.inSeconds;
|
||||
for (final segment in currentSegments!.segments) {
|
||||
final seconds = position.inSeconds;
|
||||
|
||||
if (seconds < segment.start || seconds >= segment.end) continue;
|
||||
if (seconds < segment.start || seconds >= segment.end) continue;
|
||||
|
||||
await audioPlayer.seek(Duration(seconds: segment.end + 1));
|
||||
await audioPlayer.seek(Duration(seconds: segment.end + 1));
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -122,23 +130,28 @@ class AudioPlayerStreamListeners {
|
||||
StreamSubscription subscribeToPosition() {
|
||||
String lastTrack = ""; // used to prevent multiple calls to the same track
|
||||
return audioPlayer.positionStream.listen((event) async {
|
||||
if (event < const Duration(seconds: 3) ||
|
||||
audioPlayerState.playlist.index == -1 ||
|
||||
audioPlayerState.playlist.index ==
|
||||
audioPlayerState.tracks.length - 1) {
|
||||
return;
|
||||
}
|
||||
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState.playlist.medias
|
||||
.elementAt(audioPlayerState.playlist.index + 1));
|
||||
|
||||
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ref.read(sourcedTrackProvider(nextTrack).future);
|
||||
} finally {
|
||||
lastTrack = nextTrack.track.id!;
|
||||
if (event < const Duration(seconds: 3) ||
|
||||
audioPlayerState.playlist.index == -1 ||
|
||||
audioPlayerState.playlist.index ==
|
||||
audioPlayerState.tracks.length - 1) {
|
||||
return;
|
||||
}
|
||||
final nextTrack = SpotubeMedia.fromMedia(audioPlayerState
|
||||
.playlist.medias
|
||||
.elementAt(audioPlayerState.playlist.index + 1));
|
||||
|
||||
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ref.read(sourcedTrackProvider(nextTrack).future);
|
||||
} finally {
|
||||
lastTrack = nextTrack.track.id!;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:bonsoir/bonsoir.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/services/device_info/device_info.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class ConnectClientsState {
|
||||
final List<BonsoirService> services;
|
||||
@ -37,42 +38,47 @@ class ConnectClientsNotifier extends AsyncNotifier<ConnectClientsState> {
|
||||
|
||||
final subscription = discovery.eventStream?.listen((event) {
|
||||
// ignore device itself
|
||||
if (event.service?.attributes["deviceId"] == deviceId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (event.service?.attributes["deviceId"] == deviceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case BonsoirDiscoveryEventType.discoveryServiceFound:
|
||||
state = AsyncData(state.value!.copyWith(
|
||||
services: [
|
||||
...?state.value?.services,
|
||||
event.service!,
|
||||
],
|
||||
));
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceResolved:
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
resolvedService: event.service as ResolvedBonsoirService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceLost:
|
||||
state = AsyncData(
|
||||
ConnectClientsState(
|
||||
services: state.value!.services
|
||||
.where((s) => s.name != event.service!.name)
|
||||
.toList(),
|
||||
discovery: state.value!.discovery,
|
||||
resolvedService: state.value?.resolvedService != null &&
|
||||
event.service?.name == state.value?.resolvedService?.name
|
||||
? null
|
||||
: state.value!.resolvedService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
switch (event.type) {
|
||||
case BonsoirDiscoveryEventType.discoveryServiceFound:
|
||||
state = AsyncData(state.value!.copyWith(
|
||||
services: [
|
||||
...?state.value?.services,
|
||||
event.service!,
|
||||
],
|
||||
));
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceResolved:
|
||||
state = AsyncData(
|
||||
state.value!.copyWith(
|
||||
resolvedService: event.service as ResolvedBonsoirService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case BonsoirDiscoveryEventType.discoveryServiceLost:
|
||||
state = AsyncData(
|
||||
ConnectClientsState(
|
||||
services: state.value!.services
|
||||
.where((s) => s.name != event.service!.name)
|
||||
.toList(),
|
||||
discovery: state.value!.discovery,
|
||||
resolvedService: state.value?.resolvedService != null &&
|
||||
event.service?.name ==
|
||||
state.value?.resolvedService?.name
|
||||
? null
|
||||
: state.value!.resolvedService,
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -7,11 +7,14 @@ import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class DiscordNotifier extends AsyncNotifier<void> {
|
||||
@override
|
||||
FutureOr<void> build() async {
|
||||
if (!kIsDesktop) return;
|
||||
|
||||
final enabled = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
|
||||
|
||||
@ -19,26 +22,38 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
|
||||
final subscriptions = [
|
||||
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (connected && playback.activeTrack != null) {
|
||||
await updatePresence(playback.activeTrack!);
|
||||
try {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (connected && playback.activeTrack != null) {
|
||||
await updatePresence(playback.activeTrack!);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.playerStateStream.listen((state) async {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack == null) return;
|
||||
try {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack == null) return;
|
||||
|
||||
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
|
||||
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
}),
|
||||
audioPlayer.positionStream.listen((position) async {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack != null) {
|
||||
final diff = position.inMilliseconds - lastPosition.inMilliseconds;
|
||||
if (diff > 500 || diff < -500) {
|
||||
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
|
||||
try {
|
||||
final playback = ref.read(audioPlayerProvider);
|
||||
if (playback.activeTrack != null) {
|
||||
final diff = position.inMilliseconds - lastPosition.inMilliseconds;
|
||||
if (diff > 500 || diff < -500) {
|
||||
await updatePresence(ref.read(audioPlayerProvider).activeTrack!);
|
||||
}
|
||||
}
|
||||
lastPosition = position;
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
lastPosition = position;
|
||||
})
|
||||
];
|
||||
|
||||
@ -46,6 +61,7 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
for (final subscription in subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
await clear();
|
||||
await close();
|
||||
await FlutterDiscordRPC.instance.dispose();
|
||||
});
|
||||
@ -53,12 +69,14 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
if (!enabled && FlutterDiscordRPC.instance.isConnected) {
|
||||
await clear();
|
||||
await close();
|
||||
} else {
|
||||
} else if (enabled) {
|
||||
await FlutterDiscordRPC.instance.connect(autoRetry: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updatePresence(Track track) async {
|
||||
if (!kIsDesktop) return;
|
||||
if (FlutterDiscordRPC.instance.isConnected == false) return;
|
||||
final artistNames = track.artists?.asString();
|
||||
final isPlaying = audioPlayer.isPlaying;
|
||||
final position = audioPlayer.position;
|
||||
@ -92,10 +110,12 @@ class DiscordNotifier extends AsyncNotifier<void> {
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
if (!kIsDesktop) return;
|
||||
await FlutterDiscordRPC.instance.clearActivity();
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
if (!kIsDesktop) return;
|
||||
await FlutterDiscordRPC.instance.disconnect();
|
||||
}
|
||||
}
|
||||
|
@ -23,68 +23,72 @@ class DownloadManagerProvider extends ChangeNotifier {
|
||||
$backHistory = <Track>{},
|
||||
dl = DownloadManager() {
|
||||
dl.statusStream.listen((event) async {
|
||||
final (:request, :status) = event;
|
||||
try {
|
||||
final (:request, :status) = event;
|
||||
|
||||
final track = $history.firstWhereOrNull(
|
||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||
);
|
||||
if (track == null) return;
|
||||
final track = $history.firstWhereOrNull(
|
||||
(element) => element.getUrlOfCodec(downloadCodec) == request.url,
|
||||
);
|
||||
if (track == null) return;
|
||||
|
||||
final savePath = getTrackFileUrl(track);
|
||||
// related to onFileExists
|
||||
final oldFile = File("$savePath.old");
|
||||
final savePath = getTrackFileUrl(track);
|
||||
// related to onFileExists
|
||||
final oldFile = File("$savePath.old");
|
||||
|
||||
// if download failed and old file exists, rename it back
|
||||
if ((status == DownloadStatus.failed ||
|
||||
status == DownloadStatus.canceled) &&
|
||||
await oldFile.exists()) {
|
||||
await oldFile.rename(savePath);
|
||||
// if download failed and old file exists, rename it back
|
||||
if ((status == DownloadStatus.failed ||
|
||||
status == DownloadStatus.canceled) &&
|
||||
await oldFile.exists()) {
|
||||
await oldFile.rename(savePath);
|
||||
}
|
||||
if (status != DownloadStatus.completed ||
|
||||
//? WebA audiotagging is not supported yet
|
||||
//? Although in future by converting weba to opus & then tagging it
|
||||
//? is possible using vorbis comments
|
||||
downloadCodec == SourceCodecs.weba) return;
|
||||
|
||||
final file = File(request.path);
|
||||
|
||||
if (await oldFile.exists()) {
|
||||
await oldFile.delete();
|
||||
}
|
||||
|
||||
final imageBytes = await downloadImage(
|
||||
(track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final metadata = Metadata(
|
||||
title: track.name,
|
||||
artist: track.artists?.map((a) => a.name).join(", "),
|
||||
album: track.album?.name,
|
||||
albumArtist: track.artists?.map((a) => a.name).join(", "),
|
||||
year: track.album?.releaseDate != null
|
||||
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
|
||||
: 1969,
|
||||
trackNumber: track.trackNumber,
|
||||
discNumber: track.discNumber,
|
||||
durationMs: track.durationMs?.toDouble() ?? 0.0,
|
||||
fileSize: BigInt.from(await file.length()),
|
||||
trackTotal: track.album?.tracks?.length ?? 0,
|
||||
picture: imageBytes != null
|
||||
? Picture(
|
||||
data: imageBytes,
|
||||
// Spotify images are always JPEGs
|
||||
mimeType: 'image/jpeg',
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
await MetadataGod.writeMetadata(
|
||||
file: file.path,
|
||||
metadata: metadata,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
if (status != DownloadStatus.completed ||
|
||||
//? WebA audiotagging is not supported yet
|
||||
//? Although in future by converting weba to opus & then tagging it
|
||||
//? is possible using vorbis comments
|
||||
downloadCodec == SourceCodecs.weba) return;
|
||||
|
||||
final file = File(request.path);
|
||||
|
||||
if (await oldFile.exists()) {
|
||||
await oldFile.delete();
|
||||
}
|
||||
|
||||
final imageBytes = await downloadImage(
|
||||
(track.album?.images).asUrlString(
|
||||
placeholder: ImagePlaceholder.albumArt,
|
||||
index: 1,
|
||||
),
|
||||
);
|
||||
|
||||
final metadata = Metadata(
|
||||
title: track.name,
|
||||
artist: track.artists?.map((a) => a.name).join(", "),
|
||||
album: track.album?.name,
|
||||
albumArtist: track.artists?.map((a) => a.name).join(", "),
|
||||
year: track.album?.releaseDate != null
|
||||
? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969
|
||||
: 1969,
|
||||
trackNumber: track.trackNumber,
|
||||
discNumber: track.discNumber,
|
||||
durationMs: track.durationMs?.toDouble() ?? 0.0,
|
||||
fileSize: BigInt.from(await file.length()),
|
||||
trackTotal: track.album?.tracks?.length ?? 0,
|
||||
picture: imageBytes != null
|
||||
? Picture(
|
||||
data: imageBytes,
|
||||
// Spotify images are always JPEGs
|
||||
mimeType: 'image/jpeg',
|
||||
)
|
||||
: null,
|
||||
);
|
||||
|
||||
await MetadataGod.writeMetadata(
|
||||
file: file.path,
|
||||
metadata: metadata,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -90,12 +90,18 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
fetch(arg, offset, limit) async {
|
||||
final albumsQuery = createAlbumsQuery(limit: limit, offset: offset);
|
||||
|
||||
return getAlbumsWithCount(await albumsQuery.get());
|
||||
final items = getAlbumsWithCount(await albumsQuery.get());
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: items.length == limit,
|
||||
nextOffset: offset + limit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final albums = await fetch(arg, 0, 20);
|
||||
final (items: albums, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createAlbumsQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
@ -111,9 +117,9 @@ class HistoryTopAlbumsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
return HistoryTopAlbumsState(
|
||||
items: albums,
|
||||
offset: albums.length,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,12 +55,18 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
fetch(arg, offset, limit) async {
|
||||
final playlistsQuery = createPlaylistsQuery()..limit(limit, offset: offset);
|
||||
|
||||
return getPlaylistsWithCount(await playlistsQuery.get());
|
||||
final items = getPlaylistsWithCount(await playlistsQuery.get());
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: items.length == limit,
|
||||
nextOffset: offset + limit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final playlists = await fetch(arg, 0, 20);
|
||||
final (items: playlists, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createPlaylistsQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
@ -76,9 +82,9 @@ class HistoryTopPlaylistsNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
return HistoryTopPlaylistsState(
|
||||
items: playlists,
|
||||
offset: playlists.length,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -89,12 +89,18 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
fetch(arg, offset, limit) async {
|
||||
final tracksQuery = createTracksQuery()..limit(limit, offset: offset);
|
||||
|
||||
return getTracksWithCount(await tracksQuery.get());
|
||||
final items = getTracksWithCount(await tracksQuery.get());
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: items.length == limit,
|
||||
nextOffset: offset + limit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
final tracks = await fetch(arg, 0, 20);
|
||||
final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
|
||||
|
||||
final subscription = createTracksQuery().watch().listen((event) {
|
||||
if (state.asData == null) return;
|
||||
@ -110,9 +116,9 @@ class HistoryTopTracksNotifier extends FamilyPaginatedAsyncNotifier<
|
||||
|
||||
return HistoryTopTracksState(
|
||||
items: tracks,
|
||||
offset: tracks.length,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: true,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -72,39 +73,35 @@ final localTracksProvider =
|
||||
}
|
||||
}
|
||||
|
||||
final List<Map<dynamic, dynamic>> filesWithMetadata = [];
|
||||
final List<Map<dynamic, dynamic>> filesWithMetadata = await Future.wait(
|
||||
entities.map((file) async {
|
||||
try {
|
||||
final metadata = await MetadataGod.readMetadata(file: file.path);
|
||||
|
||||
for (final file in entities) {
|
||||
try {
|
||||
final metadata = await MetadataGod.readMetadata(file: file.path);
|
||||
final imageFile = File(join(
|
||||
(await getTemporaryDirectory()).path,
|
||||
"spotube",
|
||||
basenameWithoutExtension(file.path) +
|
||||
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
||||
));
|
||||
if (!await imageFile.exists() && metadata.picture != null) {
|
||||
await imageFile.create(recursive: true);
|
||||
await imageFile.writeAsBytes(
|
||||
metadata.picture?.data ?? [],
|
||||
mode: FileMode.writeOnly,
|
||||
);
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
final imageFile = File(join(
|
||||
(await getTemporaryDirectory()).path,
|
||||
"spotube",
|
||||
basenameWithoutExtension(file.path) +
|
||||
imgMimeToExt[metadata.picture?.mimeType ?? "image/jpeg"]!,
|
||||
));
|
||||
if (!await imageFile.exists() && metadata.picture != null) {
|
||||
await imageFile.create(recursive: true);
|
||||
await imageFile.writeAsBytes(
|
||||
metadata.picture?.data ?? [],
|
||||
mode: FileMode.writeOnly,
|
||||
);
|
||||
return {"metadata": metadata, "file": file, "art": imageFile.path};
|
||||
} catch (e, stack) {
|
||||
if (e case FrbException() || TimeoutException()) {
|
||||
return {"file": file};
|
||||
}
|
||||
AppLogger.reportError(e, stack);
|
||||
return null;
|
||||
}
|
||||
|
||||
filesWithMetadata.add(
|
||||
{"metadata": metadata, "file": file, "art": imageFile.path},
|
||||
);
|
||||
} catch (e, stack) {
|
||||
if (e case FrbException() || TimeoutException()) {
|
||||
filesWithMetadata.add({"file": file});
|
||||
}
|
||||
AppLogger.reportError(e, stack);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}),
|
||||
).then((value) => value.whereNotNull().toList());
|
||||
|
||||
final tracksFromMetadata = filesWithMetadata
|
||||
.map(
|
||||
|
@ -23,19 +23,23 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
||||
|
||||
final subscription =
|
||||
database.select(database.scrobblerTable).watch().listen((event) async {
|
||||
if (event.isNotEmpty) {
|
||||
state = await AsyncValue.guard(
|
||||
() async => Scrobblenaut(
|
||||
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: event.first.username,
|
||||
passwordHash: event.first.passwordHash.value,
|
||||
try {
|
||||
if (event.isNotEmpty) {
|
||||
state = await AsyncValue.guard(
|
||||
() async => Scrobblenaut(
|
||||
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||
apiKey: Env.lastFmApiKey,
|
||||
apiSecret: Env.lastFmApiSecret,
|
||||
username: event.first.username,
|
||||
passwordHash: event.first.passwordHash.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
state = const AsyncValue.data(null);
|
||||
);
|
||||
} else {
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,13 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
|
||||
@override
|
||||
fetch(arg, offset, limit) async {
|
||||
final tracks = await spotify.albums.tracks(arg.id!).getPage(limit, offset);
|
||||
return tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
|
||||
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: !tracks.isLast,
|
||||
nextOffset: tracks.nextOffset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -39,12 +45,12 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
|
||||
ref.cacheFor();
|
||||
|
||||
ref.watch(spotifyProvider);
|
||||
final tracks = await fetch(arg, 0, 20);
|
||||
final (:items, :nextOffset, :hasMore) = await fetch(arg, 0, 20);
|
||||
return AlbumTracksState(
|
||||
items: tracks,
|
||||
offset: 0,
|
||||
items: items,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: tracks.length == 20,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,13 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
.albums(arg, country: market)
|
||||
.getPage(limit, offset);
|
||||
|
||||
return albums.items?.toList() ?? [];
|
||||
final items = albums.items?.toList() ?? [];
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: !albums.isLast,
|
||||
nextOffset: albums.nextOffset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -46,12 +52,12 @@ class ArtistAlbumsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
ref.watch(
|
||||
userPreferencesProvider.select((s) => s.market),
|
||||
);
|
||||
final albums = await fetch(arg, 0, 20);
|
||||
final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
|
||||
return ArtistAlbumsState(
|
||||
items: albums,
|
||||
offset: 0,
|
||||
items: items,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: albums.length == 20,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
(json) => PlaylistsFeatured.fromJson(json),
|
||||
).getPage(limit, offset);
|
||||
|
||||
return playlists.items?.whereNotNull().toList() ?? [];
|
||||
final items = playlists.items?.whereNotNull().toList() ?? [];
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: !playlists.isLast,
|
||||
nextOffset: playlists.nextOffset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -50,13 +56,13 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
ref.watch(userPreferencesProvider.select((s) => s.market));
|
||||
|
||||
final playlists = await fetch(arg, 0, 8);
|
||||
final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 8);
|
||||
|
||||
return CategoryPlaylistsState(
|
||||
items: playlists,
|
||||
offset: 0,
|
||||
items: items,
|
||||
offset: nextOffset,
|
||||
limit: 8,
|
||||
hasMore: playlists.length == 8,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +125,7 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
|
||||
try {
|
||||
final database = ref.watch(databaseProvider);
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final auth = await ref.watch(authenticationProvider.future);
|
||||
|
||||
if (track == null) {
|
||||
throw "No track currently";
|
||||
@ -139,11 +140,13 @@ class SyncedLyricsNotifier extends FamilyAsyncNotifier<SubtitleSimple, Track?> {
|
||||
|
||||
final token = await spotify.getCredentials();
|
||||
|
||||
if (lyrics == null || lyrics.lyrics.isEmpty) {
|
||||
if ((lyrics == null || lyrics.lyrics.isEmpty) && auth != null) {
|
||||
lyrics = await getSpotifyLyrics(token.accessToken);
|
||||
}
|
||||
|
||||
if (lyrics.lyrics.isEmpty || lyrics.lyrics.length <= 5) {
|
||||
if (lyrics == null ||
|
||||
lyrics.lyrics.isEmpty ||
|
||||
lyrics.lyrics.length <= 5) {
|
||||
lyrics = await getLRCLibLyrics();
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,16 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
|
||||
/// Filter out tracks with null id because some personal playlists
|
||||
/// may contain local tracks that are not available in the Spotify catalog
|
||||
return tracks.items
|
||||
final items = tracks.items
|
||||
?.where((track) => track.id != null && track.type == "track")
|
||||
.toList() ??
|
||||
<Track>[];
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: !tracks.isLast,
|
||||
nextOffset: tracks.nextOffset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -47,13 +53,13 @@ class PlaylistTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
ref.cacheFor();
|
||||
|
||||
ref.watch(spotifyProvider);
|
||||
final tracks = await fetch(arg, 0, 20);
|
||||
final (items: tracks, :hasMore, :nextOffset) = await fetch(arg, 0, 20);
|
||||
|
||||
return PlaylistTracksState(
|
||||
items: tracks,
|
||||
offset: 0,
|
||||
offset: nextOffset,
|
||||
limit: 20,
|
||||
hasMore: tracks.length == 20,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
|
||||
|
||||
@override
|
||||
fetch(arg, offset, limit) async {
|
||||
if (state.value == null) return [];
|
||||
if (state.value == null) {
|
||||
return (
|
||||
items: <Y>[],
|
||||
hasMore: false,
|
||||
nextOffset: 0,
|
||||
);
|
||||
}
|
||||
final results = await spotify.search
|
||||
.get(
|
||||
ref.read(searchTermStateProvider),
|
||||
@ -46,7 +52,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
|
||||
)
|
||||
.getPage(limit, offset);
|
||||
|
||||
return results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();
|
||||
final items = results.expand((e) => e.items ?? <Y>[]).toList().cast<Y>();
|
||||
|
||||
return (
|
||||
items: items,
|
||||
hasMore: items.length == limit,
|
||||
nextOffset: offset + limit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -59,13 +71,13 @@ class SearchNotifier<Y> extends AutoDisposeFamilyPaginatedAsyncNotifier<Y,
|
||||
userPreferencesProvider.select((value) => value.market),
|
||||
);
|
||||
|
||||
final results = await fetch(arg, 0, 10);
|
||||
final (:items, :hasMore, :nextOffset) = await fetch(arg, 0, 10);
|
||||
|
||||
return SearchState<Y>(
|
||||
items: results,
|
||||
offset: 0,
|
||||
items: items,
|
||||
offset: nextOffset,
|
||||
limit: 10,
|
||||
hasMore: results.length == 10,
|
||||
hasMore: hasMore,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/authentication/authentication.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/spotify/utils/json_cast.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
@ -1,10 +1,16 @@
|
||||
part of '../../spotify.dart';
|
||||
|
||||
typedef PseudoPaginatedProps<T> = ({
|
||||
List<T> items,
|
||||
int nextOffset,
|
||||
bool hasMore,
|
||||
});
|
||||
|
||||
abstract class FamilyPaginatedAsyncNotifier<
|
||||
K,
|
||||
T extends BasePaginatedState<K, dynamic>,
|
||||
A> extends FamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
|
||||
Future<List<K>> fetch(A arg, int offset, int limit);
|
||||
Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
@ -13,18 +19,18 @@ abstract class FamilyPaginatedAsyncNotifier<
|
||||
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final items = await fetch(
|
||||
final (:items, :hasMore, :nextOffset) = await fetch(
|
||||
arg,
|
||||
state.value!.offset + state.value!.limit,
|
||||
state.value!.offset,
|
||||
state.value!.limit,
|
||||
);
|
||||
return state.value!.copyWith(
|
||||
hasMore: items.length == state.value!.limit,
|
||||
hasMore: hasMore,
|
||||
items: [
|
||||
...state.value!.items,
|
||||
...items,
|
||||
],
|
||||
offset: state.value!.offset + state.value!.limit,
|
||||
offset: nextOffset,
|
||||
) as T;
|
||||
},
|
||||
);
|
||||
@ -37,16 +43,16 @@ abstract class FamilyPaginatedAsyncNotifier<
|
||||
bool hasMore = true;
|
||||
while (hasMore) {
|
||||
await update((state) async {
|
||||
final items = await fetch(
|
||||
final res = await fetch(
|
||||
arg,
|
||||
state.offset + state.limit,
|
||||
state.offset,
|
||||
state.limit,
|
||||
);
|
||||
|
||||
hasMore = items.length == state.limit;
|
||||
hasMore = res.hasMore;
|
||||
return state.copyWith(
|
||||
items: [...state.items, ...items],
|
||||
offset: state.offset + state.limit,
|
||||
items: [...state.items, ...res.items],
|
||||
offset: res.nextOffset,
|
||||
hasMore: hasMore,
|
||||
) as T;
|
||||
});
|
||||
@ -60,7 +66,7 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
K,
|
||||
T extends BasePaginatedState<K, dynamic>,
|
||||
A> extends AutoDisposeFamilyAsyncNotifier<T, A> with SpotifyMixin<T> {
|
||||
Future<List<K>> fetch(A arg, int offset, int limit);
|
||||
Future<PseudoPaginatedProps<K>> fetch(A arg, int offset, int limit);
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.value == null || !state.value!.hasMore) return;
|
||||
@ -69,18 +75,19 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final items = await fetch(
|
||||
final (:items, :hasMore, :nextOffset) = await fetch(
|
||||
arg,
|
||||
state.value!.offset + state.value!.limit,
|
||||
state.value!.offset,
|
||||
state.value!.limit,
|
||||
);
|
||||
|
||||
return state.value!.copyWith(
|
||||
hasMore: items.length == state.value!.limit,
|
||||
hasMore: hasMore,
|
||||
items: [
|
||||
...state.value!.items,
|
||||
...items,
|
||||
],
|
||||
offset: state.value!.offset + state.value!.limit,
|
||||
offset: nextOffset,
|
||||
) as T;
|
||||
},
|
||||
);
|
||||
@ -93,16 +100,16 @@ abstract class AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
bool hasMore = true;
|
||||
while (hasMore) {
|
||||
await update((state) async {
|
||||
final items = await fetch(
|
||||
final res = await fetch(
|
||||
arg,
|
||||
state.offset + state.limit,
|
||||
state.offset,
|
||||
state.limit,
|
||||
);
|
||||
|
||||
hasMore = items.length == state.limit;
|
||||
hasMore = res.hasMore;
|
||||
return state.copyWith(
|
||||
items: [...state.items, ...items],
|
||||
offset: state.offset + state.limit,
|
||||
items: [...state.items, ...res.items],
|
||||
offset: res.nextOffset,
|
||||
hasMore: hasMore,
|
||||
) as T;
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@ -41,15 +42,21 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
..where((tbl) => tbl.id.equals(0)))
|
||||
.watchSingle()
|
||||
.listen((event) async {
|
||||
state = event;
|
||||
try {
|
||||
state = event;
|
||||
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
state.systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden,
|
||||
);
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setTitleBarStyle(
|
||||
state.systemTitleBar
|
||||
? TitleBarStyle.normal
|
||||
: TitleBarStyle.hidden,
|
||||
);
|
||||
}
|
||||
|
||||
await audioPlayer.setAudioNormalization(state.normalizeAudio);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
|
||||
await audioPlayer.setAudioNormalization(state.normalizeAudio);
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
|
@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/extensions/image.dart';
|
||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_services/mobile_audio_service.dart';
|
||||
import 'package:spotube/services/audio_services/windows_audio_service.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
@ -30,8 +31,8 @@ class AudioServices with WidgetsBindingObserver {
|
||||
kIsLinux ? 'spotube' : 'com.krtirtho.Spotube',
|
||||
androidNotificationChannelName: 'Spotube',
|
||||
androidNotificationOngoing: false,
|
||||
androidNotificationIcon: "drawable/ic_launcher_monochrome",
|
||||
androidStopForegroundOnPause: false,
|
||||
androidNotificationIcon: "drawable/ic_launcher_monochrome",
|
||||
androidNotificationChannelDescription: "Spotube Media Controls",
|
||||
),
|
||||
)
|
||||
@ -73,7 +74,7 @@ class AudioServices with WidgetsBindingObserver {
|
||||
switch (state) {
|
||||
case AppLifecycleState.detached:
|
||||
deactivateSession();
|
||||
mobile?.stop();
|
||||
audioPlayer.pause();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
@ -6,6 +7,8 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/state.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class MobileAudioService extends BaseAudioHandler {
|
||||
AudioSession? session;
|
||||
@ -119,36 +122,41 @@ class MobileAudioService extends BaseAudioHandler {
|
||||
|
||||
@override
|
||||
Future<void> onTaskRemoved() async {
|
||||
await audioPlayerNotifier.stop();
|
||||
return super.onTaskRemoved();
|
||||
await audioPlayer.pause();
|
||||
if (kIsAndroid) exit(0);
|
||||
}
|
||||
|
||||
Future<PlaybackState> _transformEvent() async {
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
MediaControl.stop,
|
||||
],
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: audioPlayer.isBuffering
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
);
|
||||
try {
|
||||
return PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
audioPlayer.isPlaying ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
MediaControl.stop,
|
||||
],
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
playing: audioPlayer.isPlaying,
|
||||
updatePosition: audioPlayer.position,
|
||||
bufferedPosition: audioPlayer.bufferedPosition,
|
||||
shuffleMode: audioPlayer.isShuffled == true
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none,
|
||||
repeatMode: switch (audioPlayer.loopMode) {
|
||||
PlaylistMode.loop => AudioServiceRepeatMode.all,
|
||||
PlaylistMode.single => AudioServiceRepeatMode.one,
|
||||
_ => AudioServiceRepeatMode.none,
|
||||
},
|
||||
processingState: audioPlayer.isBuffering
|
||||
? AudioProcessingState.loading
|
||||
: AudioProcessingState.ready,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
|
||||
class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
final _connectionStreamController = StreamController<bool>.broadcast();
|
||||
@ -16,17 +17,21 @@ class ConnectionCheckerService with WidgetsBindingObserver {
|
||||
Timer? timer;
|
||||
|
||||
onConnectivityChanged.listen((connected) {
|
||||
if (!connected && timer == null) {
|
||||
timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (WidgetsBinding.instance.lifecycleState ==
|
||||
AppLifecycleState.paused) {
|
||||
return;
|
||||
}
|
||||
await isConnected;
|
||||
});
|
||||
} else {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
try {
|
||||
if (!connected && timer == null) {
|
||||
timer = Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (WidgetsBinding.instance.lifecycleState ==
|
||||
AppLifecycleState.paused) {
|
||||
return;
|
||||
}
|
||||
await isConnected;
|
||||
});
|
||||
} else {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
AppLogger.reportError(e, stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
78
pubspec.lock
78
pubspec.lock
@ -458,7 +458,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
dbus:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
|
||||
@ -506,14 +506,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
dots_indicator:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dots_indicator
|
||||
sha256: f1599baa429936ba87f06ae5f2adc920a367b16d08f74db58c3d0f6e93bcdb5c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
draggable_scrollbar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -822,54 +814,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
flutter_keyboard_visibility:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_keyboard_visibility_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_linux
|
||||
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_keyboard_visibility_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_macos
|
||||
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_keyboard_visibility_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_platform_interface
|
||||
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_web
|
||||
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
flutter_keyboard_visibility_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility_windows
|
||||
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -1062,10 +1006,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: c5fa45fa502ee880839e3b2152d987c44abae26d064a2376d4aad434cf0f7b15
|
||||
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.1.3"
|
||||
version: "14.2.7"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1271,14 +1215,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
introduction_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: introduction_screen
|
||||
sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.14"
|
||||
io:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -1784,14 +1720,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
retry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: retry
|
||||
sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -3,7 +3,7 @@ description: Open source Spotify client that doesn't require Premium nor uses El
|
||||
|
||||
publish_to: "none"
|
||||
|
||||
version: 3.8.0+33
|
||||
version: 3.8.1+34
|
||||
|
||||
homepage: https://spotube.krtirtho.dev
|
||||
repository: https://github.com/KRTirtho/spotube
|
||||
@ -23,7 +23,6 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
collection: ^1.15.0
|
||||
curved_navigation_bar: ^1.0.3
|
||||
dbus: ^0.7.8
|
||||
desktop_webview_window:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/flutter-plugins.git
|
||||
@ -52,7 +51,6 @@ dependencies:
|
||||
flutter_svg: ^1.1.6
|
||||
form_validator: ^2.1.1
|
||||
fuzzywuzzy: ^1.1.6
|
||||
go_router: 12.1.3 # Stuck on this https://github.com/flutter/flutter/issues/140869
|
||||
google_fonts: ^6.2.1
|
||||
hive: ^2.2.3
|
||||
hive_flutter: ^1.1.0
|
||||
@ -60,7 +58,6 @@ dependencies:
|
||||
html: ^0.15.1
|
||||
image_picker: ^1.1.0
|
||||
intl: any
|
||||
introduction_screen: ^3.1.14
|
||||
json_annotation: ^4.8.1
|
||||
logger: ^2.0.2
|
||||
media_kit: ^1.1.10+1
|
||||
@ -137,7 +134,7 @@ dependencies:
|
||||
sqlite3_flutter_libs: ^0.5.23
|
||||
sqlite3: ^2.4.3
|
||||
encrypt: ^5.0.3
|
||||
retry: ^3.1.2
|
||||
go_router: ^14.2.7
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.9
|
||||
|
@ -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
|
||||
SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
|
||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
|
Loading…
Reference in New Issue
Block a user