Compare commits

...

11 Commits

Author SHA1 Message Date
CodeShakingSheep
52e1ee7625
Merge 56bfce06f1 into d3edf07ac9 2025-04-09 16:18:48 +05:30
Kingkor Roy Tirtho
d3edf07ac9 fix: default accent color in orange but it shows blue in settings 2025-04-07 16:16:43 +06:00
Kingkor Roy Tirtho
8fc319d980 fix(mobile): dialogs in bottom sheet are not opening 2025-04-07 14:53:05 +06:00
Seungmin Kim
e986baa0aa
chore: revise filter for ISRC search (#2614) 2025-04-07 13:12:45 +06:00
Kingkor Roy Tirtho
8a7f5c4008 chore: fix weird hovered mode on track tile options 2025-03-28 22:32:05 +06:00
Kingkor Roy Tirtho
9d2ad1c626 chore: upgrade shadcn_flutter version 2025-03-28 22:24:23 +06:00
Kingkor Roy Tirtho
b74c2eab8f fix: calling /track/:streamId endpoint causes active sourced track to be anything 2025-03-28 20:57:46 +06:00
Seungmin Kim
2c4cc94985
feat: add ISRC track search for YouTube (#2594)
* Add ISRC track search for YouTube

* Do not probe Song.Link when ISRC results are valid, fix rate limit
2025-03-28 19:10:54 +06:00
CodeShakingSheep
56bfce06f1 Remove one more empthy path in website SVG 2024-06-20 23:12:03 -05:00
CodeShakingSheep
5f5c055606 Optimize website SVG 2024-06-20 23:10:32 -05:00
CodeShakingSheep
2bc04325b1 Optimize SVG icons 2024-06-20 23:03:22 -05:00
43 changed files with 3963 additions and 266 deletions

View File

@ -9,6 +9,7 @@
"fuzzywuzzy",
"gapless",
"instrumentalness",
"isrc",
"Mpris",
"RGBO",
"riverpod",

View File

@ -3,4 +3,17 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="${applicationName}"
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name_en"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<!-- Disable Impeller -->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
</application>
</manifest>

View File

@ -316,34 +316,4 @@
style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>
id="path5502" /></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -326,34 +326,4 @@
style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>
id="path5502" /></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

View File

@ -94,6 +94,7 @@ abstract class FakeData {
..trackNumber = 1
..type = "type"
..uri = "uri"
..externalIds = externalIds
..isPlayable = true
..explicit = false
..linkedFrom = trackLink;

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart' show showModalBottomSheet;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -26,7 +25,7 @@ class AdaptiveMenuButton<T> extends MenuButton {
/// An adaptive widget that shows a [PopupMenuButton] when screen size is above
/// or equal to 640px
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
/// In smaller screen, a [IconButton] with a [openDrawer] is shown
class AdaptivePopSheetList<T> extends StatelessWidget {
final List<AdaptiveMenuButton<T>> Function(BuildContext context) items;
final Widget? icon;
@ -39,7 +38,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
final Offset offset;
final ButtonVariance variance;
final AbstractButtonStyle variance;
const AdaptivePopSheetList({
super.key,
@ -92,23 +91,23 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
// ),
position: position,
builder: (context) {
return DropdownMenu(
return WidgetStatesProvider.boundary(
child: DropdownMenu(
children: childrenModified(context),
),
);
},
).future;
return;
}
showModalBottomSheet(
await openDrawer(
context: context,
enableDrag: true,
draggable: true,
showDragHandle: true,
useRootNavigator: true,
shape: RoundedRectangleBorder(
position: OverlayPosition.bottom,
borderRadius: context.theme.borderRadiusMd,
),
backgroundColor: context.theme.colorScheme.card,
transformBackdrop: false,
builder: (context) {
final children = childrenModified(context);
return ListView.builder(
@ -125,7 +124,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
onPressed: () {
data.onPressed?.call(context);
if (data.autoClose) {
Navigator.of(context).pop();
closeDrawer(context);
}
},
leading: data.leading,

View File

@ -13,7 +13,7 @@ class HeartButton extends HookConsumerWidget {
final IconData? icon;
final Color? color;
final String? tooltip;
final ButtonVariance variance;
final AbstractButtonStyle variance;
final ButtonSize size;
const HeartButton({
required this.isLiked,

View File

@ -1,7 +1,6 @@
import 'dart:math';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/extensions/button_variance.dart';
class ShadcnWindowButton extends StatelessWidget {
final Widget icon;
@ -22,7 +21,7 @@ class ShadcnWindowButton extends StatelessWidget {
height: 32,
child: IconButton(
variance: ButtonVariance.ghost.copyWith(
decoration: (context, states) {
decoration: (context, states, value) {
final decoration = ButtonVariance.ghost.decoration(context, states)
as BoxDecoration;
if (hoverBackgroundColor != null &&

View File

@ -74,6 +74,26 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
ref.watch(presentationStateProvider(options.collection).notifier);
final selectedTracks = state.selectedTracks;
Future<void> actionDownloadTracks({
required BuildContext context,
required List<Track> tracks,
required String action,
}) async {
final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool>(
context: context,
builder: (context) {
return const ConfirmDownloadDialog();
},
) ??
false);
if (confirmed != true) return;
downloader.batchAddToQueue(tracks);
notifier.deselectAllTracks();
if (!context.mounted) return;
showToastForAction(context, action, tracks.length);
}
return AdaptivePopSheetList(
tooltip: context.l10n.more_actions,
headings: [
@ -95,22 +115,12 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
switch (action) {
case "download":
{
final confirmed = audioSource == AudioSource.piped ||
(await showDialog<bool?>(
await actionDownloadTracks(
context: context,
builder: (context) {
return const ConfirmDownloadDialog();
},
) ??
false);
if (confirmed != true) return;
downloader.batchAddToQueue(tracks);
notifier.deselectAllTracks();
if (!context.mounted) return;
showToastForAction(context, action, tracks.length);
tracks: tracks,
action: action,
);
break;
}
case "add-to-playlist":
{
if (context.mounted) {

View File

@ -57,7 +57,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.shuffle_playlist),
),
).call,
child: IconButton.secondary(
icon: isLoading
? const Center(
@ -73,7 +73,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_to_queue),
),
).call,
child: IconButton.secondary(
icon: const Icon(SpotubeIcons.queueAdd),
enabled: !isLoading && !isActive,
@ -126,7 +126,7 @@ class TrackPresentationTopSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.share),
),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.share),
size: ButtonSize.small,

View File

@ -91,24 +91,14 @@ class TrackOptions extends HookConsumerWidget {
) {
/// showDialog doesn't work for some reason. So we have to
/// manually push a Dialog Route in the Navigator to get it working
Navigator.push(
context,
DialogRoute(
alignment: Alignment.bottomCenter,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) {
return Center(
child: PlaylistAddTrackDialog(
return PlaylistAddTrackDialog(
tracks: [track],
openFromPlaylist: playlistId,
),
);
},
),
);
}
@ -338,6 +328,7 @@ class TrackOptions extends HookConsumerWidget {
}
},
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
variance: ButtonVariance.outline,
headings: [
Basic(
leading: AspectRatio(

View File

@ -5,7 +5,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.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:spotify/spotify.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/ui/button_tile.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/duration.dart';
import 'package:spotube/extensions/image.dart';
@ -108,7 +107,7 @@ class TrackTile extends HookConsumerWidget {
? ButtonVariance.destructive
: ButtonVariance.ghost)
.copyWith(
padding: (context, states) =>
padding: (context, states, value) =>
const EdgeInsets.symmetric(vertical: 8, horizontal: 0),
),
leading: Row(
@ -229,7 +228,8 @@ class TrackTile extends HookConsumerWidget {
Flexible(
child: Button(
style: ButtonVariance.link.copyWith(
padding: (context, states) => EdgeInsets.zero,
padding: (context, states, value) =>
EdgeInsets.zero,
),
onPressed: () {
context

View File

@ -9,7 +9,7 @@ class ButtonTile extends StatelessWidget {
final VoidCallback? onPressed;
final VoidCallback? onLongPress;
final bool selected;
final ButtonVariance style;
final AbstractButtonStyle style;
final EdgeInsets? padding;
const ButtonTile({

View File

@ -4,7 +4,9 @@ import 'dart:typed_data';
import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.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/logger/logger.dart';
extension TrackExtensions on Track {
Track fromFile(
@ -67,27 +69,40 @@ extension TrackExtensions on Track {
}
}
extension TrackSimpleExtensions on TrackSimple {
Track asTrack(AlbumSimple album) {
extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
try {
final spotify = ref.read(spotifyProvider);
final tracks = await spotify.invoke(
(api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
return tracks.toList();
} catch (e, stack) {
// Ignore errors and create the track locally
AppLogger.reportError(e, stack);
List<Track> tracks = [];
for (final trackSimple in this) {
Track track = Track();
track.name = name;
track.album = album;
track.artists = artists;
track.availableMarkets = availableMarkets;
track.discNumber = discNumber;
track.durationMs = durationMs;
track.explicit = explicit;
track.externalUrls = externalUrls;
track.href = href;
track.id = id;
track.isPlayable = isPlayable;
track.linkedFrom = linkedFrom;
track.name = name;
track.previewUrl = previewUrl;
track.trackNumber = trackNumber;
track.type = type;
track.uri = uri;
return track;
track.name = trackSimple.name;
track.artists = trackSimple.artists;
track.availableMarkets = trackSimple.availableMarkets;
track.discNumber = trackSimple.discNumber;
track.durationMs = trackSimple.durationMs;
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;
}
}
}

View File

@ -62,7 +62,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 4;
int get schemaVersion => 5;
@override
MigrationStrategy get migration {
@ -87,6 +87,33 @@ class AppDatabase extends _$AppDatabase {
schema.preferencesTable.youtubeClientEngine,
);
},
from4To5: (m, schema) async {
final columnName = schema.preferencesTable.accentColorScheme
.escapedNameFor(SqlDialect.sqlite);
final columnNameOld =
'"${schema.preferencesTable.accentColorScheme.name}_old"';
final tableName = schema.preferencesTable.actualTableName;
await customStatement(
"ALTER TABLE $tableName "
"RENAME COLUMN $columnName to $columnNameOld",
);
await customStatement(
"ALTER TABLE $tableName "
"ADD COLUMN $columnName TEXT NOT NULL DEFAULT 'Orange:0xFFf97315'",
);
await customStatement(
"UPDATE $tableName "
"SET $columnName = $columnNameOld",
);
await customStatement(
"ALTER TABLE $tableName "
"DROP COLUMN $columnNameOld",
);
await customStatement(
"UPDATE $tableName "
"SET $columnName = 'Orange:0xFFf97315' WHERE $columnName = 'Blue:0xFF2196F3'",
);
},
),
);
}

View File

@ -666,7 +666,7 @@ class $PreferencesTableTable extends PreferencesTable
'accent_color_scheme', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant("Blue:0xFF2196F3"))
defaultValue: const Constant("Orange:0xFFf97315"))
.withConverter<SpotubeColor>(
$PreferencesTableTable.$converteraccentColorScheme);
static const VerificationMeta _layoutModeMeta =

View File

@ -2,7 +2,7 @@
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter/material.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import
@ -1188,10 +1188,232 @@ i1.GeneratedColumn<String> _column_54(String aliasedName) =>
i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name));
final class Schema5 extends i0.VersionedSchema {
Schema5({required super.database}) : super(version: 5);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
playlistTable,
playlistMediaTable,
historyTable,
lyricsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape12 preferencesTable = Shape12(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_7,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_55,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_23,
_column_24,
_column_25,
_column_26,
_column_54,
_column_27,
_column_28,
_column_29,
_column_30,
_column_31,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 sourceMatchTable = Shape5(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_38,
_column_39,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 audioPlayerStateTable = Shape6(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 playlistTable = Shape7(
source: i0.VersionedTable(
entityName: 'playlist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_44,
_column_45,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 playlistMediaTable = Shape8(
source: i0.VersionedTable(
entityName: 'playlist_media_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_46,
_column_47,
_column_48,
_column_49,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
}
i1.GeneratedColumn<String> _column_55(String aliasedName) =>
i1.GeneratedColumn<String>('accent_color_scheme', aliasedName, false,
type: i1.DriftSqlType.string,
defaultValue: const Constant("Orange:0xFFf97315"));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@ -1210,6 +1432,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
case 4:
final schema = Schema5(database: database);
final migrator = i1.Migrator(database, schema);
await from4To5(migrator, schema);
return 5;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@ -1220,10 +1447,12 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
from4To5: from4To5,
));

View File

@ -79,7 +79,7 @@ class PreferencesTable extends Table {
TextColumn get closeBehavior => textEnum<CloseBehavior>()
.withDefault(Constant(CloseBehavior.close.name))();
TextColumn get accentColorScheme => text()
.withDefault(const Constant("Blue:0xFF2196F3"))
.withDefault(const Constant("Orange:0xFFf97315"))
.map(const SpotubeColorConverter())();
TextColumn get layoutMode =>
textEnum<LayoutMode>().withDefault(Constant(LayoutMode.adaptive.name))();
@ -130,7 +130,7 @@ class PreferencesTable extends Table {
systemTitleBar: false,
skipNonMusic: false,
closeBehavior: CloseBehavior.close,
accentColorScheme: SpotubeColor(Colors.blue.value, name: "Blue"),
accentColorScheme: SpotubeColor(Colors.orange.value, name: "Orange"),
layoutMode: LayoutMode.adaptive,
locale: const Locale("system", "system"),
market: Market.US,

View File

@ -54,7 +54,7 @@ class AlbumCard extends HookConsumerWidget {
Future<List<Track>> fetchAllTrack() async {
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);
return ref.read(albumTracksProvider(album).notifier).fetchAll();

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.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:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_hooks/flutter_hooks.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:spotube/collections/assets.gen.dart';
@ -132,7 +132,7 @@ class PlayerView extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.details),
),
).call,
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.info, size: 18),
onPressed: currentTrack == null

View File

@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.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:spotube/collections/routes.gr.dart';
@ -82,7 +82,7 @@ class PlayerActions extends HookConsumerWidget {
children: [
if (showQueue)
Tooltip(
tooltip: TooltipContainer(child: Text(context.l10n.queue)),
tooltip: TooltipContainer(child: Text(context.l10n.queue)).call,
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.queue),
enabled: playlist.activeTrack != null,
@ -119,7 +119,8 @@ class PlayerActions extends HookConsumerWidget {
if (!isLocalTrack)
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.alternative_track_sources)),
child: Text(context.l10n.alternative_track_sources),
).call,
child: IconButton.ghost(
enabled: playlist.activeTrack != null,
icon: const Icon(SpotubeIcons.alternativeRoute),
@ -160,7 +161,8 @@ class PlayerActions extends HookConsumerWidget {
else
Tooltip(
tooltip:
TooltipContainer(child: Text(context.l10n.download_track)),
TooltipContainer(child: Text(context.l10n.download_track))
.call,
child: IconButton.ghost(
icon: Icon(
isDownloaded ? SpotubeIcons.done : SpotubeIcons.download,

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.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/intents.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.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:spotube/collections/intents.dart';
import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.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:spotube/collections/env.dart';
import 'package:spotube/collections/routes.gr.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter_hooks/flutter_hooks.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:spotube/collections/assets.gen.dart';

View File

@ -1,7 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/services.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:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,6 +1,6 @@
import 'package:auto_route/auto_route.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:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';

View File

@ -1,6 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.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:spotube/components/titlebar/titlebar.dart';

View File

@ -2,7 +2,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';

View File

@ -125,28 +125,34 @@ class SearchPage extends HookConsumerWidget {
child: TextField(
autofocus: true,
controller: controller,
leading:
const Icon(SpotubeIcons.search),
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
trailing: AnimatedCrossFade(
duration:
const Duration(milliseconds: 300),
crossFadeState:
controller.text.isNotEmpty
features: [
const InputFeature.leading(
Icon(SpotubeIcons.search),
),
InputFeature.trailing(
AnimatedCrossFade(
duration: const Duration(
milliseconds: 300),
crossFadeState: controller
.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon:
const Icon(SpotubeIcons.close),
icon: const Icon(
SpotubeIcons.close),
onPressed: () {
controller.clear();
},
),
secondChild: const SizedBox.square(
secondChild:
const SizedBox.square(
dimension: 28),
),
)
],
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
onSubmitted: onSubmitted,
),
),

View File

@ -11,7 +11,7 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart';
@ -106,7 +106,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
@ -261,7 +261,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,

View File

@ -128,7 +128,10 @@ class ServerPlaybackRoutes {
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
.refreshStreamingUrl();
if (playlist.activeTrack?.id == sourcedTrack?.id &&
sourcedTrack != null) {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
}
return await dio.get<Uint8List>(
sourcedTrack!.url,
@ -199,7 +202,10 @@ class ServerPlaybackRoutes {
? activeSourcedTrack
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
if (playlist.activeTrack?.id == sourcedTrack?.id &&
sourcedTrack != null) {
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
}
final (bytes: audioBytes, response: res) =
await streamTrack(sourcedTrack!, request.headers);

View File

@ -33,7 +33,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
final tracks = await spotify.invoke(
(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 (
items: items,

View File

@ -90,9 +90,9 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
Future<void> reset() async {
final db = ref.read(databaseProvider);
final query = db.update(db.preferencesTable)..where((t) => t.id.equals(0));
final query = db.update(db.preferencesTable);
await query.replace(PreferencesTableCompanion.insert());
await query.replace(PreferencesTableCompanion.insert(id: const Value(0)));
}
static Future<String> getMusicCacheDir() async {

View File

@ -236,34 +236,77 @@ class YoutubeSourcedTrack extends SourcedTrack {
.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'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
final spWords = track.name!
.toLowerCase()
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
// Single word and duration match with 3 second tolerance
if (ytWords.any((word) => spWords.contains(word)) &&
(videoInfo.duration - track.duration!)
.abs().inMilliseconds <= 3000) {
return videoInfo;
}
return null;
})
.whereType<YoutubeVideoInfo>()
.toList());
}
}
return isrcResults;
}
static Future<List<SiblingType>> fetchSiblings({
required Track track,
required Ref ref,
}) async {
final links = await SongLinkService.links(track.id!);
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
final videoResults = <YoutubeVideoInfo>[];
if (ytLink?.url != null
// allows to fetch siblings more results for already sourced track
&&
track is! SourcedTrack) {
if (track is! SourcedTrack) {
final isrcResults = await fetchFromIsrc(
track: track,
ref: ref,
);
videoResults.addAll(isrcResults);
if (isrcResults.isEmpty) {
final links = await SongLinkService.links(track.id!);
final ytLink = links.firstWhereOrNull(
(link) => link.platform == "youtube",
);
if (ytLink?.url != null) {
try {
return [
await toSiblingType(
0,
YoutubeVideoInfo.fromVideo(
await ref.read(youtubeEngineProvider).getVideo(
Uri.parse(ytLink!.url!).queryParameters["v"]!,
),
),
ref,
)
];
videoResults.add(
YoutubeVideoInfo.fromVideo(await ref
.read(youtubeEngineProvider)
.getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)),
);
} on VideoUnplayableException catch (e, stack) {
// Ignore this error and continue with the search
AppLogger.reportError(e, stack);
}
}
}
}
final query = SourcedTrack.getSearchTerm(track);
@ -271,20 +314,27 @@ class YoutubeSourcedTrack extends SourcedTrack {
await ref.read(youtubeEngineProvider).searchVideos(query);
if (ServiceUtils.onlyContainsEnglish(query)) {
return await Future.wait(searchResults
.map(YoutubeVideoInfo.fromVideo)
.mapIndexed((index, info) => toSiblingType(index, info, ref)));
}
final rankedSiblings = rankResults(
videoResults
.addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
} else {
videoResults.addAll(rankResults(
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
track,
);
));
}
final seenIds = <String>{};
int index = 0;
return await Future.wait(
rankedSiblings
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
);
videoResults.map((videoResult) async {
// 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

View File

@ -32,6 +32,7 @@ function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
target_compile_options(${TARGET} PRIVATE -Wno-error=deprecated-declarations)
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")

View File

@ -2012,10 +2012,10 @@ packages:
dependency: "direct main"
description:
name: shadcn_flutter
sha256: "1e5f40484a42217a69af254952168783d1305025d56dabc45ab16396dba84d5e"
sha256: "2b6faf9a93628469c29a534e653295e26781f2799efe5dc971b91e91062ebf52"
url: "https://pub.dev"
source: hosted
version: "0.0.26"
version: "0.0.32"
shared_preferences:
dependency: "direct main"
description:
@ -2449,10 +2449,10 @@ packages:
dependency: "direct main"
description:
name: tray_manager
sha256: f231031c5c0eb4ad514e18ddaab27a912ddbe50335c594bc28fb0f9972ab6a84
sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
url: "https://pub.dev"
source: hosted
version: "0.3.1"
version: "0.4.0"
type_plus:
dependency: transitive
description:

View File

@ -102,7 +102,7 @@ dependencies:
ref: dart-3-support
url: https://github.com/KRTirtho/scrobblenaut.git
scroll_to_index: ^3.0.1
shadcn_flutter: ^0.0.26
shadcn_flutter: ^0.0.32
shared_preferences: ^2.2.3
shelf: ^1.4.1
shelf_router: ^1.1.4
@ -120,7 +120,7 @@ dependencies:
test: ^1.25.7
timezone: ^0.10.0
titlebar_buttons: ^1.0.0
tray_manager: ^0.3.0
tray_manager: ^0.4.0
url_launcher: ^6.2.6
uuid: ^4.4.0
version: ^3.0.2

View File

@ -3,27 +3,30 @@
// ignore_for_file: type=lint
import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'schema_v4.dart' as v4;
import 'schema_v3.dart' as v3;
import 'schema_v2.dart' as v2;
import 'schema_v5.dart' as v5;
import 'schema_v1.dart' as v1;
import 'schema_v2.dart' as v2;
import 'schema_v4.dart' as v4;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
switch (version) {
case 4:
return v4.DatabaseAtV4(db);
case 3:
return v3.DatabaseAtV3(db);
case 2:
return v2.DatabaseAtV2(db);
case 5:
return v5.DatabaseAtV5(db);
case 1:
return v1.DatabaseAtV1(db);
case 2:
return v2.DatabaseAtV2(db);
case 4:
return v4.DatabaseAtV4(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4];
static const versions = const [1, 2, 3, 4, 5];
}

File diff suppressed because it is too large Load Diff

View File

@ -316,34 +316,4 @@
style="stroke-width:3.28861" /><path
d="m 188.76763,155.437 v 98.42 c 0,5.867 4.741,10.605 10.60501,10.605 5.854,0 10.605,-4.738 10.605,-10.605 v -98.42 c 0,-5.856 -4.751,-10.605 -10.605,-10.605 -5.86401,0 -10.60501,4.744 -10.60501,10.605 z"
style="fill:none;stroke:url(#linearGradient5506);stroke-width:9.80924px;stroke-linecap:round;stroke-linejoin:round"
id="path5502" /></g><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g240" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g242" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g244" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g246" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g248" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g250" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g252" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g254" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g256" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g258" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g260" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g262" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g264" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g266" /><g
transform="matrix(0.972684,0,0,0.972684,193.06382,142.14148)"
id="g268" /></svg>
id="path5502" /></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB