mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 15:39:41 +00:00
Compare commits
7 Commits
aecf36d2f9
...
d35c521865
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35c521865 | ||
|
|
8a7f5c4008 | ||
|
|
9d2ad1c626 | ||
|
|
b74c2eab8f | ||
|
|
2c4cc94985 | ||
|
|
9163f1abe0 | ||
|
|
227909787d |
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -9,6 +9,7 @@
|
|||||||
"fuzzywuzzy",
|
"fuzzywuzzy",
|
||||||
"gapless",
|
"gapless",
|
||||||
"instrumentalness",
|
"instrumentalness",
|
||||||
|
"isrc",
|
||||||
"Mpris",
|
"Mpris",
|
||||||
"RGBO",
|
"RGBO",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
|
|||||||
@ -94,6 +94,7 @@ abstract class FakeData {
|
|||||||
..trackNumber = 1
|
..trackNumber = 1
|
||||||
..type = "type"
|
..type = "type"
|
||||||
..uri = "uri"
|
..uri = "uri"
|
||||||
|
..externalIds = externalIds
|
||||||
..isPlayable = true
|
..isPlayable = true
|
||||||
..explicit = false
|
..explicit = false
|
||||||
..linkedFrom = trackLink;
|
..linkedFrom = trackLink;
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
|
|
||||||
final Offset offset;
|
final Offset offset;
|
||||||
|
|
||||||
final ButtonVariance variance;
|
final AbstractButtonStyle variance;
|
||||||
|
|
||||||
const AdaptivePopSheetList({
|
const AdaptivePopSheetList({
|
||||||
super.key,
|
super.key,
|
||||||
@ -92,8 +92,10 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
|
|||||||
// ),
|
// ),
|
||||||
position: position,
|
position: position,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return DropdownMenu(
|
return WidgetStatesProvider.boundary(
|
||||||
children: childrenModified(context),
|
child: DropdownMenu(
|
||||||
|
children: childrenModified(context),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).future;
|
).future;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ class HeartButton extends HookConsumerWidget {
|
|||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final String? tooltip;
|
final String? tooltip;
|
||||||
final ButtonVariance variance;
|
final AbstractButtonStyle variance;
|
||||||
final ButtonSize size;
|
final ButtonSize size;
|
||||||
const HeartButton({
|
const HeartButton({
|
||||||
required this.isLiked,
|
required this.isLiked,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/extensions/button_variance.dart';
|
|
||||||
|
|
||||||
class ShadcnWindowButton extends StatelessWidget {
|
class ShadcnWindowButton extends StatelessWidget {
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
@ -22,7 +21,7 @@ class ShadcnWindowButton extends StatelessWidget {
|
|||||||
height: 32,
|
height: 32,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
variance: ButtonVariance.ghost.copyWith(
|
variance: ButtonVariance.ghost.copyWith(
|
||||||
decoration: (context, states) {
|
decoration: (context, states, value) {
|
||||||
final decoration = ButtonVariance.ghost.decoration(context, states)
|
final decoration = ButtonVariance.ghost.decoration(context, states)
|
||||||
as BoxDecoration;
|
as BoxDecoration;
|
||||||
if (hoverBackgroundColor != null &&
|
if (hoverBackgroundColor != null &&
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
@ -17,7 +17,6 @@ import 'package:spotube/components/links/link_text.dart';
|
|||||||
import 'package:spotube/components/track_tile/track_options.dart';
|
import 'package:spotube/components/track_tile/track_options.dart';
|
||||||
import 'package:spotube/components/ui/button_tile.dart';
|
import 'package:spotube/components/ui/button_tile.dart';
|
||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/button_variance.dart';
|
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/duration.dart';
|
import 'package:spotube/extensions/duration.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
@ -108,7 +107,7 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
? ButtonVariance.destructive
|
? ButtonVariance.destructive
|
||||||
: ButtonVariance.ghost)
|
: ButtonVariance.ghost)
|
||||||
.copyWith(
|
.copyWith(
|
||||||
padding: (context, states) =>
|
padding: (context, states, value) =>
|
||||||
const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
|
||||||
),
|
),
|
||||||
leading: Row(
|
leading: Row(
|
||||||
@ -229,7 +228,8 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Button(
|
child: Button(
|
||||||
style: ButtonVariance.link.copyWith(
|
style: ButtonVariance.link.copyWith(
|
||||||
padding: (context, states) => EdgeInsets.zero,
|
padding: (context, states, value) =>
|
||||||
|
EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context
|
context
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class ButtonTile extends StatelessWidget {
|
|||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final VoidCallback? onLongPress;
|
final VoidCallback? onLongPress;
|
||||||
final bool selected;
|
final bool selected;
|
||||||
final ButtonVariance style;
|
final AbstractButtonStyle style;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const ButtonTile({
|
const ButtonTile({
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import 'dart:typed_data';
|
|||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
extension TrackExtensions on Track {
|
extension TrackExtensions on Track {
|
||||||
Track fromFile(
|
Track fromFile(
|
||||||
@ -67,27 +69,40 @@ extension TrackExtensions on Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrackSimpleExtensions on TrackSimple {
|
extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
|
||||||
Track asTrack(AlbumSimple album) {
|
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
|
||||||
Track track = Track();
|
try {
|
||||||
track.name = name;
|
final spotify = ref.read(spotifyProvider);
|
||||||
track.album = album;
|
final tracks = await spotify.invoke(
|
||||||
track.artists = artists;
|
(api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
|
||||||
track.availableMarkets = availableMarkets;
|
return tracks.toList();
|
||||||
track.discNumber = discNumber;
|
} catch (e, stack) {
|
||||||
track.durationMs = durationMs;
|
// Ignore errors and create the track locally
|
||||||
track.explicit = explicit;
|
AppLogger.reportError(e, stack);
|
||||||
track.externalUrls = externalUrls;
|
|
||||||
track.href = href;
|
List<Track> tracks = [];
|
||||||
track.id = id;
|
for (final trackSimple in this) {
|
||||||
track.isPlayable = isPlayable;
|
Track track = Track();
|
||||||
track.linkedFrom = linkedFrom;
|
track.album = album;
|
||||||
track.name = name;
|
track.name = trackSimple.name;
|
||||||
track.previewUrl = previewUrl;
|
track.artists = trackSimple.artists;
|
||||||
track.trackNumber = trackNumber;
|
track.availableMarkets = trackSimple.availableMarkets;
|
||||||
track.type = type;
|
track.discNumber = trackSimple.discNumber;
|
||||||
track.uri = uri;
|
track.durationMs = trackSimple.durationMs;
|
||||||
return track;
|
track.explicit = trackSimple.explicit;
|
||||||
|
track.externalUrls = trackSimple.externalUrls;
|
||||||
|
track.href = trackSimple.href;
|
||||||
|
track.id = trackSimple.id;
|
||||||
|
track.isPlayable = trackSimple.isPlayable;
|
||||||
|
track.linkedFrom = trackSimple.linkedFrom;
|
||||||
|
track.previewUrl = trackSimple.previewUrl;
|
||||||
|
track.trackNumber = trackSimple.trackNumber;
|
||||||
|
track.type = trackSimple.type;
|
||||||
|
track.uri = trackSimple.uri;
|
||||||
|
tracks.add(track);
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
Future<List<Track>> fetchAllTrack() async {
|
Future<List<Track>> fetchAllTrack() async {
|
||||||
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
||||||
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
return album.tracks!.asTracks(album, ref);
|
||||||
}
|
}
|
||||||
await ref.read(albumTracksProvider(album).future);
|
await ref.read(albumTracksProvider(album).future);
|
||||||
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:spotify/spotify.dart' hide Image;
|
import 'package:spotify/spotify.dart' hide Image;
|
||||||
import 'package:spotube/collections/env.dart';
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
@ -8,6 +8,7 @@ import 'package:spotube/components/image/universal_image.dart';
|
|||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/extensions/string.dart';
|
import 'package:spotube/extensions/string.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
import 'package:spotube/provider/spotify/spotify.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
import 'package:stroke_text/stroke_text.dart';
|
import 'package:stroke_text/stroke_text.dart';
|
||||||
|
|
||||||
class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
||||||
@ -21,8 +22,10 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
final w = kIsDesktop ? 20 : 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 115 * theme.scaling,
|
width: (115 + w) * theme.scaling,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.background.withAlpha(75),
|
color: theme.colorScheme.background.withAlpha(75),
|
||||||
borderRadius: theme.borderRadiusMd,
|
borderRadius: theme.borderRadiusMd,
|
||||||
@ -65,7 +68,7 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
|||||||
ref.watch(playlistImageProvider(playlist.id!));
|
ref.watch(playlistImageProvider(playlist.id!));
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 100 * theme.scaling,
|
height: 100 * theme.scaling,
|
||||||
width: 100 * theme.scaling,
|
width: (100 + w) * theme.scaling,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
@ -107,14 +110,14 @@ class GenreSectionCardPlaylistCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
height: 100 * theme.scaling,
|
height: 100 * theme.scaling,
|
||||||
width: 100 * theme.scaling,
|
width: (100 + w) * theme.scaling,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
playlist.name!,
|
playlist.name!,
|
||||||
maxLines: 2,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
).semiBold().small(),
|
).xSmall().bold(),
|
||||||
if (playlist.description != null)
|
if (playlist.description != null)
|
||||||
Text(
|
Text(
|
||||||
playlist.description?.unescapeHtml().cleanHtml() ?? "",
|
playlist.description?.unescapeHtml().cleanHtml() ?? "",
|
||||||
|
|||||||
@ -99,91 +99,99 @@ class LocalFolderItem extends HookConsumerWidget {
|
|||||||
itemCount: tracks.length,
|
itemCount: tracks.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final track = tracks[index];
|
final track = tracks[index];
|
||||||
return UniversalImage(
|
return Expanded(
|
||||||
path: (track.album?.images).asUrlString(
|
child: UniversalImage(
|
||||||
placeholder: ImagePlaceholder.albumArt,
|
path: (track.album?.images).asUrlString(
|
||||||
|
placeholder: ImagePlaceholder.albumArt,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
fit: BoxFit.cover,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Stack(
|
Expanded(
|
||||||
children: [
|
child: Stack(
|
||||||
Column(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Center(
|
children: [
|
||||||
child: Text(
|
Center(
|
||||||
isDownloadFolder
|
child: Flexible(
|
||||||
? context.l10n.downloads
|
child: Text(
|
||||||
: isCacheFolder
|
isDownloadFolder
|
||||||
? context.l10n.cache_folder.capitalize()
|
? context.l10n.downloads
|
||||||
: basename(folder),
|
: isCacheFolder
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
? context.l10n.cache_folder.capitalize()
|
||||||
textAlign: TextAlign.center,
|
: basename(folder),
|
||||||
maxLines: 1,
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
overflow: TextOverflow.ellipsis,
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 2,
|
||||||
|
runSpacing: 2,
|
||||||
|
children: [
|
||||||
|
for (final MapEntry(key: index, value: segment)
|
||||||
|
in segments.asMap().entries)
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
if (index != 0) const TextSpan(text: "/ "),
|
||||||
|
TextSpan(text: segment)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
).xSmall().muted(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!isDownloadFolder && !isCacheFolder)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: IconButton.ghost(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
size: ButtonSize.small,
|
||||||
|
onPressed: () {
|
||||||
|
showDropdown(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return DropdownMenu(
|
||||||
|
children: [
|
||||||
|
MenuButton(
|
||||||
|
leading: Icon(SpotubeIcons.folderRemove,
|
||||||
|
color: colorScheme.destructive),
|
||||||
|
child: Text(
|
||||||
|
context.l10n.remove_library_location),
|
||||||
|
onPressed: (context) {
|
||||||
|
final libraryLocations = ref
|
||||||
|
.read(userPreferencesProvider)
|
||||||
|
.localLibraryLocation;
|
||||||
|
ref
|
||||||
|
.read(userPreferencesProvider.notifier)
|
||||||
|
.setLocalLibraryLocation(
|
||||||
|
libraryLocations
|
||||||
|
.where((e) => e != folder)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
],
|
||||||
spacing: 2,
|
),
|
||||||
runSpacing: 2,
|
|
||||||
children: [
|
|
||||||
for (final MapEntry(key: index, value: segment)
|
|
||||||
in segments.asMap().entries)
|
|
||||||
Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
if (index != 0) const TextSpan(text: "/ "),
|
|
||||||
TextSpan(text: segment),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
maxLines: 2,
|
|
||||||
).xSmall().muted(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (!isDownloadFolder && !isCacheFolder)
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.topRight,
|
|
||||||
child: IconButton.ghost(
|
|
||||||
icon: const Icon(Icons.more_vert),
|
|
||||||
size: ButtonSize.small,
|
|
||||||
onPressed: () {
|
|
||||||
showDropdown(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return DropdownMenu(
|
|
||||||
children: [
|
|
||||||
MenuButton(
|
|
||||||
leading: Icon(SpotubeIcons.folderRemove,
|
|
||||||
color: colorScheme.destructive),
|
|
||||||
child:
|
|
||||||
Text(context.l10n.remove_library_location),
|
|
||||||
onPressed: (context) {
|
|
||||||
final libraryLocations = ref
|
|
||||||
.read(userPreferencesProvider)
|
|
||||||
.localLibraryLocation;
|
|
||||||
ref
|
|
||||||
.read(userPreferencesProvider.notifier)
|
|
||||||
.setLocalLibraryLocation(
|
|
||||||
libraryLocations
|
|
||||||
.where((e) => e != folder)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
|
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
import 'package:sliding_up_panel/sliding_up_panel.dart';
|
||||||
import 'package:spotube/collections/intents.dart';
|
import 'package:spotube/collections/intents.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:spotify/spotify.dart' hide Offset, Image;
|
import 'package:spotify/spotify.dart' hide Offset, Image;
|
||||||
import 'package:spotube/collections/env.dart';
|
import 'package:spotube/collections/env.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
|
||||||
import 'package:spotube/collections/assets.gen.dart';
|
import 'package:spotube/collections/assets.gen.dart';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:auto_size_text/auto_size_text.dart';
|
import 'package:auto_size_text/auto_size_text.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:spotube/collections/fake.dart';
|
import 'package:spotube/collections/fake.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
|||||||
@ -125,28 +125,34 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
leading:
|
features: [
|
||||||
const Icon(SpotubeIcons.search),
|
const InputFeature.leading(
|
||||||
textInputAction: TextInputAction.search,
|
Icon(SpotubeIcons.search),
|
||||||
placeholder: Text(context.l10n.search),
|
),
|
||||||
trailing: AnimatedCrossFade(
|
InputFeature.trailing(
|
||||||
duration:
|
AnimatedCrossFade(
|
||||||
const Duration(milliseconds: 300),
|
duration: const Duration(
|
||||||
crossFadeState:
|
milliseconds: 300),
|
||||||
controller.text.isNotEmpty
|
crossFadeState: controller
|
||||||
|
.text.isNotEmpty
|
||||||
? CrossFadeState.showFirst
|
? CrossFadeState.showFirst
|
||||||
: CrossFadeState.showSecond,
|
: CrossFadeState.showSecond,
|
||||||
firstChild: IconButton.ghost(
|
firstChild: IconButton.ghost(
|
||||||
size: ButtonSize.small,
|
size: ButtonSize.small,
|
||||||
icon:
|
icon: const Icon(
|
||||||
const Icon(SpotubeIcons.close),
|
SpotubeIcons.close),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.clear();
|
controller.clear();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
secondChild: const SizedBox.square(
|
secondChild:
|
||||||
dimension: 28),
|
const SizedBox.square(
|
||||||
),
|
dimension: 28),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
placeholder: Text(context.l10n.search),
|
||||||
onSubmitted: onSubmitted,
|
onSubmitted: onSubmitted,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import 'package:form_builder_validators/form_builder_validators.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/components/form/text_form_field.dart';
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
@ -106,7 +106,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: TooltipContainer(
|
tooltip: TooltipContainer(
|
||||||
child: Text(context.l10n.add_custom_url),
|
child: Text(context.l10n.add_custom_url),
|
||||||
),
|
).call,
|
||||||
child: IconButton.outline(
|
child: IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.edit),
|
icon: const Icon(SpotubeIcons.edit),
|
||||||
size: ButtonSize.small,
|
size: ButtonSize.small,
|
||||||
@ -261,7 +261,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
Tooltip(
|
Tooltip(
|
||||||
tooltip: TooltipContainer(
|
tooltip: TooltipContainer(
|
||||||
child: Text(context.l10n.add_custom_url),
|
child: Text(context.l10n.add_custom_url),
|
||||||
),
|
).call,
|
||||||
child: IconButton.outline(
|
child: IconButton.outline(
|
||||||
icon: const Icon(SpotubeIcons.edit),
|
icon: const Icon(SpotubeIcons.edit),
|
||||||
size: ButtonSize.small,
|
size: ButtonSize.small,
|
||||||
|
|||||||
@ -128,7 +128,10 @@ class ServerPlaybackRoutes {
|
|||||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
||||||
.refreshStreamingUrl();
|
.refreshStreamingUrl();
|
||||||
|
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
if (playlist.activeTrack?.id == sourcedTrack?.id &&
|
||||||
|
sourcedTrack != null) {
|
||||||
|
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||||
|
}
|
||||||
|
|
||||||
return await dio.get<Uint8List>(
|
return await dio.get<Uint8List>(
|
||||||
sourcedTrack!.url,
|
sourcedTrack!.url,
|
||||||
@ -199,7 +202,10 @@ class ServerPlaybackRoutes {
|
|||||||
? activeSourcedTrack
|
? activeSourcedTrack
|
||||||
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
|
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
|
||||||
|
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
if (playlist.activeTrack?.id == sourcedTrack?.id &&
|
||||||
|
sourcedTrack != null) {
|
||||||
|
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||||
|
}
|
||||||
|
|
||||||
final (bytes: audioBytes, response: res) =
|
final (bytes: audioBytes, response: res) =
|
||||||
await streamTrack(sourcedTrack!, request.headers);
|
await streamTrack(sourcedTrack!, request.headers);
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
|
|||||||
final tracks = await spotify.invoke(
|
final tracks = await spotify.invoke(
|
||||||
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
|
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
|
||||||
);
|
);
|
||||||
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
|
final items = await tracks.items!.asTracks(arg, ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
items: items,
|
items: items,
|
||||||
|
|||||||
@ -236,32 +236,76 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
|
||||||
|
required Track track,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
final isrcResults = <YoutubeVideoInfo>[];
|
||||||
|
final isrc = track.externalIds?.isrc;
|
||||||
|
if (isrc != null && isrc.isNotEmpty) {
|
||||||
|
final searchedVideos =
|
||||||
|
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
|
||||||
|
if (searchedVideos.isNotEmpty) {
|
||||||
|
isrcResults.addAll(searchedVideos
|
||||||
|
.map<YoutubeVideoInfo>(YoutubeVideoInfo.fromVideo)
|
||||||
|
.map((YoutubeVideoInfo videoInfo) {
|
||||||
|
final ytWords = videoInfo.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty);
|
||||||
|
final spWords = track.name!
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'\((.*)\)'), '')
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty);
|
||||||
|
// Word match to filter out unrelated results
|
||||||
|
final matchCount =
|
||||||
|
ytWords.where((word) => spWords.contains(word)).length;
|
||||||
|
if (matchCount > spWords.length ~/ 2) {
|
||||||
|
return videoInfo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.whereType<YoutubeVideoInfo>()
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isrcResults;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final links = await SongLinkService.links(track.id!);
|
final videoResults = <YoutubeVideoInfo>[];
|
||||||
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
|
||||||
|
|
||||||
if (ytLink?.url != null
|
if (track is! SourcedTrack) {
|
||||||
// allows to fetch siblings more results for already sourced track
|
final isrcResults = await fetchFromIsrc(
|
||||||
&&
|
track: track,
|
||||||
track is! SourcedTrack) {
|
ref: ref,
|
||||||
try {
|
);
|
||||||
return [
|
|
||||||
await toSiblingType(
|
videoResults.addAll(isrcResults);
|
||||||
0,
|
|
||||||
YoutubeVideoInfo.fromVideo(
|
if (isrcResults.isEmpty) {
|
||||||
await ref.read(youtubeEngineProvider).getVideo(
|
final links = await SongLinkService.links(track.id!);
|
||||||
Uri.parse(ytLink!.url!).queryParameters["v"]!,
|
final ytLink = links.firstWhereOrNull(
|
||||||
),
|
(link) => link.platform == "youtube",
|
||||||
),
|
);
|
||||||
ref,
|
if (ytLink?.url != null) {
|
||||||
)
|
try {
|
||||||
];
|
videoResults.add(
|
||||||
} on VideoUnplayableException catch (e, stack) {
|
YoutubeVideoInfo.fromVideo(await ref
|
||||||
// Ignore this error and continue with the search
|
.read(youtubeEngineProvider)
|
||||||
AppLogger.reportError(e, stack);
|
.getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)),
|
||||||
|
);
|
||||||
|
} on VideoUnplayableException catch (e, stack) {
|
||||||
|
// Ignore this error and continue with the search
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,20 +315,27 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
await ref.read(youtubeEngineProvider).searchVideos(query);
|
await ref.read(youtubeEngineProvider).searchVideos(query);
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||||
return await Future.wait(searchResults
|
videoResults
|
||||||
.map(YoutubeVideoInfo.fromVideo)
|
.addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
|
||||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)));
|
} else {
|
||||||
|
videoResults.addAll(rankResults(
|
||||||
|
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
||||||
|
track,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final rankedSiblings = rankResults(
|
final seenIds = <String>{};
|
||||||
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
int index = 0;
|
||||||
track,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
rankedSiblings
|
videoResults.map((videoResult) async {
|
||||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
|
// Deduplicate results
|
||||||
);
|
if (!seenIds.contains(videoResult.id)) {
|
||||||
|
seenIds.add(videoResult.id);
|
||||||
|
return await toSiblingType(index++, videoResult, ref);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
).then((s) => s.whereType<SiblingType>().toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@ -32,6 +32,7 @@ function(APPLY_STANDARD_SETTINGS TARGET)
|
|||||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
|||||||
@ -2012,10 +2012,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shadcn_flutter
|
name: shadcn_flutter
|
||||||
sha256: "1e5f40484a42217a69af254952168783d1305025d56dabc45ab16396dba84d5e"
|
sha256: "2b6faf9a93628469c29a534e653295e26781f2799efe5dc971b91e91062ebf52"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.26"
|
version: "0.0.32"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2449,10 +2449,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: tray_manager
|
name: tray_manager
|
||||||
sha256: f231031c5c0eb4ad514e18ddaab27a912ddbe50335c594bc28fb0f9972ab6a84
|
sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
version: "0.4.0"
|
||||||
type_plus:
|
type_plus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -102,7 +102,7 @@ dependencies:
|
|||||||
ref: dart-3-support
|
ref: dart-3-support
|
||||||
url: https://github.com/KRTirtho/scrobblenaut.git
|
url: https://github.com/KRTirtho/scrobblenaut.git
|
||||||
scroll_to_index: ^3.0.1
|
scroll_to_index: ^3.0.1
|
||||||
shadcn_flutter: ^0.0.26
|
shadcn_flutter: ^0.0.32
|
||||||
shared_preferences: ^2.2.3
|
shared_preferences: ^2.2.3
|
||||||
shelf: ^1.4.1
|
shelf: ^1.4.1
|
||||||
shelf_router: ^1.1.4
|
shelf_router: ^1.1.4
|
||||||
@ -120,7 +120,7 @@ dependencies:
|
|||||||
test: ^1.25.7
|
test: ^1.25.7
|
||||||
timezone: ^0.10.0
|
timezone: ^0.10.0
|
||||||
titlebar_buttons: ^1.0.0
|
titlebar_buttons: ^1.0.0
|
||||||
tray_manager: ^0.3.0
|
tray_manager: ^0.4.0
|
||||||
url_launcher: ^6.2.6
|
url_launcher: ^6.2.6
|
||||||
uuid: ^4.4.0
|
uuid: ^4.4.0
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user