mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: use providers in generate playlist
This commit is contained in:
parent
b76e265f23
commit
ac8121647b
@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Search;
|
||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||
import 'package:spotube/pages/album/album.dart';
|
||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||
@ -96,8 +97,7 @@ final routerProvider = Provider((ref) {
|
||||
path: "result",
|
||||
pageBuilder: (context, state) => SpotubePage(
|
||||
child: PlaylistGenerateResultPage(
|
||||
state:
|
||||
state.extra as PlaylistGenerateResultRouteState,
|
||||
state: state.extra as GeneratePlaylistProviderInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -3,9 +3,22 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'recommendation_seeds.freezed.dart';
|
||||
part 'recommendation_seeds.g.dart';
|
||||
|
||||
@freezed
|
||||
class GeneratePlaylistProviderInput with _$GeneratePlaylistProviderInput {
|
||||
factory GeneratePlaylistProviderInput({
|
||||
Iterable<String>? seedArtists,
|
||||
Iterable<String>? seedGenres,
|
||||
Iterable<String>? seedTracks,
|
||||
required int limit,
|
||||
RecommendationSeeds? max,
|
||||
RecommendationSeeds? min,
|
||||
RecommendationSeeds? target,
|
||||
}) = _GeneratePlaylistProviderInput;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RecommendationSeeds with _$RecommendationSeeds {
|
||||
factory RecommendationSeeds(
|
||||
factory RecommendationSeeds({
|
||||
num? acousticness,
|
||||
num? danceability,
|
||||
@JsonKey(name: "duration_ms") num? durationMs,
|
||||
@ -20,7 +33,7 @@ class RecommendationSeeds with _$RecommendationSeeds {
|
||||
num? tempo,
|
||||
@JsonKey(name: "time_signature") num? timeSignature,
|
||||
num? valence,
|
||||
) = _RecommendationSeeds;
|
||||
}) = _RecommendationSeeds;
|
||||
|
||||
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecommendationSeedsFromJson(json);
|
||||
|
||||
@ -14,6 +14,316 @@ T _$identity<T>(T value) => value;
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$GeneratePlaylistProviderInput {
|
||||
Iterable<String>? get seedArtists => throw _privateConstructorUsedError;
|
||||
Iterable<String>? get seedGenres => throw _privateConstructorUsedError;
|
||||
Iterable<String>? get seedTracks => throw _privateConstructorUsedError;
|
||||
int get limit => throw _privateConstructorUsedError;
|
||||
RecommendationSeeds? get max => throw _privateConstructorUsedError;
|
||||
RecommendationSeeds? get min => throw _privateConstructorUsedError;
|
||||
RecommendationSeeds? get target => throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$GeneratePlaylistProviderInputCopyWith<GeneratePlaylistProviderInput>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $GeneratePlaylistProviderInputCopyWith<$Res> {
|
||||
factory $GeneratePlaylistProviderInputCopyWith(
|
||||
GeneratePlaylistProviderInput value,
|
||||
$Res Function(GeneratePlaylistProviderInput) then) =
|
||||
_$GeneratePlaylistProviderInputCopyWithImpl<$Res,
|
||||
GeneratePlaylistProviderInput>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{Iterable<String>? seedArtists,
|
||||
Iterable<String>? seedGenres,
|
||||
Iterable<String>? seedTracks,
|
||||
int limit,
|
||||
RecommendationSeeds? max,
|
||||
RecommendationSeeds? min,
|
||||
RecommendationSeeds? target});
|
||||
|
||||
$RecommendationSeedsCopyWith<$Res>? get max;
|
||||
$RecommendationSeedsCopyWith<$Res>? get min;
|
||||
$RecommendationSeedsCopyWith<$Res>? get target;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
|
||||
$Val extends GeneratePlaylistProviderInput>
|
||||
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
|
||||
_$GeneratePlaylistProviderInputCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? seedArtists = freezed,
|
||||
Object? seedGenres = freezed,
|
||||
Object? seedTracks = freezed,
|
||||
Object? limit = null,
|
||||
Object? max = freezed,
|
||||
Object? min = freezed,
|
||||
Object? target = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
seedArtists: freezed == seedArtists
|
||||
? _value.seedArtists
|
||||
: seedArtists // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
seedGenres: freezed == seedGenres
|
||||
? _value.seedGenres
|
||||
: seedGenres // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
seedTracks: freezed == seedTracks
|
||||
? _value.seedTracks
|
||||
: seedTracks // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
limit: null == limit
|
||||
? _value.limit
|
||||
: limit // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
max: freezed == max
|
||||
? _value.max
|
||||
: max // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
min: freezed == min
|
||||
? _value.min
|
||||
: min // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
target: freezed == target
|
||||
? _value.target
|
||||
: target // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
) as $Val);
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$RecommendationSeedsCopyWith<$Res>? get max {
|
||||
if (_value.max == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $RecommendationSeedsCopyWith<$Res>(_value.max!, (value) {
|
||||
return _then(_value.copyWith(max: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$RecommendationSeedsCopyWith<$Res>? get min {
|
||||
if (_value.min == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $RecommendationSeedsCopyWith<$Res>(_value.min!, (value) {
|
||||
return _then(_value.copyWith(min: value) as $Val);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$RecommendationSeedsCopyWith<$Res>? get target {
|
||||
if (_value.target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $RecommendationSeedsCopyWith<$Res>(_value.target!, (value) {
|
||||
return _then(_value.copyWith(target: value) as $Val);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$GeneratePlaylistProviderInputImplCopyWith<$Res>
|
||||
implements $GeneratePlaylistProviderInputCopyWith<$Res> {
|
||||
factory _$$GeneratePlaylistProviderInputImplCopyWith(
|
||||
_$GeneratePlaylistProviderInputImpl value,
|
||||
$Res Function(_$GeneratePlaylistProviderInputImpl) then) =
|
||||
__$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{Iterable<String>? seedArtists,
|
||||
Iterable<String>? seedGenres,
|
||||
Iterable<String>? seedTracks,
|
||||
int limit,
|
||||
RecommendationSeeds? max,
|
||||
RecommendationSeeds? min,
|
||||
RecommendationSeeds? target});
|
||||
|
||||
@override
|
||||
$RecommendationSeedsCopyWith<$Res>? get max;
|
||||
@override
|
||||
$RecommendationSeedsCopyWith<$Res>? get min;
|
||||
@override
|
||||
$RecommendationSeedsCopyWith<$Res>? get target;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$GeneratePlaylistProviderInputImplCopyWithImpl<$Res>
|
||||
extends _$GeneratePlaylistProviderInputCopyWithImpl<$Res,
|
||||
_$GeneratePlaylistProviderInputImpl>
|
||||
implements _$$GeneratePlaylistProviderInputImplCopyWith<$Res> {
|
||||
__$$GeneratePlaylistProviderInputImplCopyWithImpl(
|
||||
_$GeneratePlaylistProviderInputImpl _value,
|
||||
$Res Function(_$GeneratePlaylistProviderInputImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? seedArtists = freezed,
|
||||
Object? seedGenres = freezed,
|
||||
Object? seedTracks = freezed,
|
||||
Object? limit = null,
|
||||
Object? max = freezed,
|
||||
Object? min = freezed,
|
||||
Object? target = freezed,
|
||||
}) {
|
||||
return _then(_$GeneratePlaylistProviderInputImpl(
|
||||
seedArtists: freezed == seedArtists
|
||||
? _value.seedArtists
|
||||
: seedArtists // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
seedGenres: freezed == seedGenres
|
||||
? _value.seedGenres
|
||||
: seedGenres // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
seedTracks: freezed == seedTracks
|
||||
? _value.seedTracks
|
||||
: seedTracks // ignore: cast_nullable_to_non_nullable
|
||||
as Iterable<String>?,
|
||||
limit: null == limit
|
||||
? _value.limit
|
||||
: limit // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
max: freezed == max
|
||||
? _value.max
|
||||
: max // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
min: freezed == min
|
||||
? _value.min
|
||||
: min // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
target: freezed == target
|
||||
? _value.target
|
||||
: target // ignore: cast_nullable_to_non_nullable
|
||||
as RecommendationSeeds?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$GeneratePlaylistProviderInputImpl
|
||||
implements _GeneratePlaylistProviderInput {
|
||||
_$GeneratePlaylistProviderInputImpl(
|
||||
{this.seedArtists,
|
||||
this.seedGenres,
|
||||
this.seedTracks,
|
||||
required this.limit,
|
||||
this.max,
|
||||
this.min,
|
||||
this.target});
|
||||
|
||||
@override
|
||||
final Iterable<String>? seedArtists;
|
||||
@override
|
||||
final Iterable<String>? seedGenres;
|
||||
@override
|
||||
final Iterable<String>? seedTracks;
|
||||
@override
|
||||
final int limit;
|
||||
@override
|
||||
final RecommendationSeeds? max;
|
||||
@override
|
||||
final RecommendationSeeds? min;
|
||||
@override
|
||||
final RecommendationSeeds? target;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GeneratePlaylistProviderInput(seedArtists: $seedArtists, seedGenres: $seedGenres, seedTracks: $seedTracks, limit: $limit, max: $max, min: $min, target: $target)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$GeneratePlaylistProviderInputImpl &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.seedArtists, seedArtists) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.seedGenres, seedGenres) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other.seedTracks, seedTracks) &&
|
||||
(identical(other.limit, limit) || other.limit == limit) &&
|
||||
(identical(other.max, max) || other.max == max) &&
|
||||
(identical(other.min, min) || other.min == min) &&
|
||||
(identical(other.target, target) || other.target == target));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(seedArtists),
|
||||
const DeepCollectionEquality().hash(seedGenres),
|
||||
const DeepCollectionEquality().hash(seedTracks),
|
||||
limit,
|
||||
max,
|
||||
min,
|
||||
target);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$GeneratePlaylistProviderInputImplCopyWith<
|
||||
_$GeneratePlaylistProviderInputImpl>
|
||||
get copyWith => __$$GeneratePlaylistProviderInputImplCopyWithImpl<
|
||||
_$GeneratePlaylistProviderInputImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _GeneratePlaylistProviderInput
|
||||
implements GeneratePlaylistProviderInput {
|
||||
factory _GeneratePlaylistProviderInput(
|
||||
{final Iterable<String>? seedArtists,
|
||||
final Iterable<String>? seedGenres,
|
||||
final Iterable<String>? seedTracks,
|
||||
required final int limit,
|
||||
final RecommendationSeeds? max,
|
||||
final RecommendationSeeds? min,
|
||||
final RecommendationSeeds? target}) = _$GeneratePlaylistProviderInputImpl;
|
||||
|
||||
@override
|
||||
Iterable<String>? get seedArtists;
|
||||
@override
|
||||
Iterable<String>? get seedGenres;
|
||||
@override
|
||||
Iterable<String>? get seedTracks;
|
||||
@override
|
||||
int get limit;
|
||||
@override
|
||||
RecommendationSeeds? get max;
|
||||
@override
|
||||
RecommendationSeeds? get min;
|
||||
@override
|
||||
RecommendationSeeds? get target;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$GeneratePlaylistProviderInputImplCopyWith<
|
||||
_$GeneratePlaylistProviderInputImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
RecommendationSeeds _$RecommendationSeedsFromJson(Map<String, dynamic> json) {
|
||||
return _RecommendationSeeds.fromJson(json);
|
||||
}
|
||||
@ -207,59 +517,59 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
|
||||
Object? valence = freezed,
|
||||
}) {
|
||||
return _then(_$RecommendationSeedsImpl(
|
||||
freezed == acousticness
|
||||
acousticness: freezed == acousticness
|
||||
? _value.acousticness
|
||||
: acousticness // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == danceability
|
||||
danceability: freezed == danceability
|
||||
? _value.danceability
|
||||
: danceability // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == durationMs
|
||||
durationMs: freezed == durationMs
|
||||
? _value.durationMs
|
||||
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == energy
|
||||
energy: freezed == energy
|
||||
? _value.energy
|
||||
: energy // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == instrumentalness
|
||||
instrumentalness: freezed == instrumentalness
|
||||
? _value.instrumentalness
|
||||
: instrumentalness // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == key
|
||||
key: freezed == key
|
||||
? _value.key
|
||||
: key // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == liveness
|
||||
liveness: freezed == liveness
|
||||
? _value.liveness
|
||||
: liveness // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == loudness
|
||||
loudness: freezed == loudness
|
||||
? _value.loudness
|
||||
: loudness // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == mode
|
||||
mode: freezed == mode
|
||||
? _value.mode
|
||||
: mode // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == popularity
|
||||
popularity: freezed == popularity
|
||||
? _value.popularity
|
||||
: popularity // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == speechiness
|
||||
speechiness: freezed == speechiness
|
||||
? _value.speechiness
|
||||
: speechiness // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == tempo
|
||||
tempo: freezed == tempo
|
||||
? _value.tempo
|
||||
: tempo // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == timeSignature
|
||||
timeSignature: freezed == timeSignature
|
||||
? _value.timeSignature
|
||||
: timeSignature // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
freezed == valence
|
||||
valence: freezed == valence
|
||||
? _value.valence
|
||||
: valence // ignore: cast_nullable_to_non_nullable
|
||||
as num?,
|
||||
@ -271,7 +581,7 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
||||
_$RecommendationSeedsImpl(
|
||||
this.acousticness,
|
||||
{this.acousticness,
|
||||
this.danceability,
|
||||
@JsonKey(name: "duration_ms") this.durationMs,
|
||||
this.energy,
|
||||
@ -284,7 +594,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
||||
this.speechiness,
|
||||
this.tempo,
|
||||
@JsonKey(name: "time_signature") this.timeSignature,
|
||||
this.valence);
|
||||
this.valence});
|
||||
|
||||
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$RecommendationSeedsImplFromJson(json);
|
||||
@ -391,7 +701,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
||||
|
||||
abstract class _RecommendationSeeds implements RecommendationSeeds {
|
||||
factory _RecommendationSeeds(
|
||||
final num? acousticness,
|
||||
{final num? acousticness,
|
||||
final num? danceability,
|
||||
@JsonKey(name: "duration_ms") final num? durationMs,
|
||||
final num? energy,
|
||||
@ -404,7 +714,7 @@ abstract class _RecommendationSeeds implements RecommendationSeeds {
|
||||
final num? speechiness,
|
||||
final num? tempo,
|
||||
@JsonKey(name: "time_signature") final num? timeSignature,
|
||||
final num? valence) = _$RecommendationSeedsImpl;
|
||||
final num? valence}) = _$RecommendationSeedsImpl;
|
||||
|
||||
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
|
||||
_$RecommendationSeedsImpl.fromJson;
|
||||
|
||||
@ -9,20 +9,20 @@ part of 'recommendation_seeds.dart';
|
||||
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$RecommendationSeedsImpl(
|
||||
json['acousticness'] as num?,
|
||||
json['danceability'] as num?,
|
||||
json['duration_ms'] as num?,
|
||||
json['energy'] as num?,
|
||||
json['instrumentalness'] as num?,
|
||||
json['key'] as num?,
|
||||
json['liveness'] as num?,
|
||||
json['loudness'] as num?,
|
||||
json['mode'] as num?,
|
||||
json['popularity'] as num?,
|
||||
json['speechiness'] as num?,
|
||||
json['tempo'] as num?,
|
||||
json['time_signature'] as num?,
|
||||
json['valence'] as num?,
|
||||
acousticness: json['acousticness'] as num?,
|
||||
danceability: json['danceability'] as num?,
|
||||
durationMs: json['duration_ms'] as num?,
|
||||
energy: json['energy'] as num?,
|
||||
instrumentalness: json['instrumentalness'] as num?,
|
||||
key: json['key'] as num?,
|
||||
liveness: json['liveness'] as num?,
|
||||
loudness: json['loudness'] as num?,
|
||||
mode: json['mode'] as num?,
|
||||
popularity: json['popularity'] as num?,
|
||||
speechiness: json['speechiness'] as num?,
|
||||
tempo: json['tempo'] as num?,
|
||||
timeSignature: json['time_signature'] as num?,
|
||||
valence: json['valence'] as num?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$RecommendationSeedsImplToJson(
|
||||
|
||||
@ -15,11 +15,10 @@ import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/pages/library/playlist_generate/playlist_generate_result.dart';
|
||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
import 'package:spotube/provider/spotify_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/queries/playlist.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
|
||||
@ -35,7 +34,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
final textTheme = theme.textTheme;
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
final genresCollection = useQueries.category.genreSeeds(ref);
|
||||
final genresCollection = ref.watch(categoryGenresProvider);
|
||||
|
||||
final limit = useValueNotifier<int>(10);
|
||||
final market = useValueNotifier<Market>(preferences.recommendationMarket);
|
||||
@ -51,22 +50,9 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
5 - genres.value.length - artists.value.length - tracks.value.length;
|
||||
|
||||
// Dial (int 0-1) attributes
|
||||
final acousticness = useState<RecommendationAttribute>(zeroValues);
|
||||
final danceability = useState<RecommendationAttribute>(zeroValues);
|
||||
final energy = useState<RecommendationAttribute>(zeroValues);
|
||||
final instrumentalness = useState<RecommendationAttribute>(zeroValues);
|
||||
final key = useState<RecommendationAttribute>(zeroValues);
|
||||
final liveness = useState<RecommendationAttribute>(zeroValues);
|
||||
final loudness = useState<RecommendationAttribute>(zeroValues);
|
||||
final popularity = useState<RecommendationAttribute>(zeroValues);
|
||||
final speechiness = useState<RecommendationAttribute>(zeroValues);
|
||||
final valence = useState<RecommendationAttribute>(zeroValues);
|
||||
|
||||
// Field editable attributes
|
||||
final tempo = useState<RecommendationAttribute>(zeroValues);
|
||||
final durationMs = useState<RecommendationAttribute>(zeroValues);
|
||||
final mode = useState<RecommendationAttribute>(zeroValues);
|
||||
final timeSignature = useState<RecommendationAttribute>(zeroValues);
|
||||
final min = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||
final max = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||
final target = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||
|
||||
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
|
||||
seeds: artists,
|
||||
@ -204,7 +190,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final genreSelector = MultiSelectField<String>(
|
||||
options: genresCollection.data ?? [],
|
||||
options: genresCollection.value ?? [],
|
||||
selectedOptions: genres.value,
|
||||
getValueForOption: (option) => option,
|
||||
onSelected: (value) {
|
||||
@ -356,88 +342,213 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
const SizedBox(height: 16),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.acousticness),
|
||||
values: acousticness.value,
|
||||
values: (
|
||||
target: target.value.acousticness?.toDouble() ?? 0,
|
||||
min: min.value.acousticness?.toDouble() ?? 0,
|
||||
max: max.value.acousticness?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
acousticness.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
acousticness: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
acousticness: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
acousticness: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.danceability),
|
||||
values: danceability.value,
|
||||
values: (
|
||||
target: target.value.danceability?.toDouble() ?? 0,
|
||||
min: min.value.danceability?.toDouble() ?? 0,
|
||||
max: max.value.danceability?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
danceability.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
danceability: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
danceability: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
danceability: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.energy),
|
||||
values: energy.value,
|
||||
values: (
|
||||
target: target.value.energy?.toDouble() ?? 0,
|
||||
min: min.value.energy?.toDouble() ?? 0,
|
||||
max: max.value.energy?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
energy.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
energy: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
energy: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
energy: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.instrumentalness),
|
||||
values: instrumentalness.value,
|
||||
values: (
|
||||
target:
|
||||
target.value.instrumentalness?.toDouble() ?? 0,
|
||||
min: min.value.instrumentalness?.toDouble() ?? 0,
|
||||
max: max.value.instrumentalness?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
instrumentalness.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
instrumentalness: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
instrumentalness: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
instrumentalness: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.liveness),
|
||||
values: liveness.value,
|
||||
values: (
|
||||
target: target.value.liveness?.toDouble() ?? 0,
|
||||
min: min.value.liveness?.toDouble() ?? 0,
|
||||
max: max.value.liveness?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
liveness.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
liveness: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
liveness: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
liveness: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.loudness),
|
||||
values: loudness.value,
|
||||
values: (
|
||||
target: target.value.loudness?.toDouble() ?? 0,
|
||||
min: min.value.loudness?.toDouble() ?? 0,
|
||||
max: max.value.loudness?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
loudness.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
loudness: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
loudness: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
loudness: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.speechiness),
|
||||
values: speechiness.value,
|
||||
values: (
|
||||
target: target.value.speechiness?.toDouble() ?? 0,
|
||||
min: min.value.speechiness?.toDouble() ?? 0,
|
||||
max: max.value.speechiness?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
speechiness.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
speechiness: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
speechiness: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
speechiness: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.valence),
|
||||
values: valence.value,
|
||||
values: (
|
||||
target: target.value.valence?.toDouble() ?? 0,
|
||||
min: min.value.valence?.toDouble() ?? 0,
|
||||
max: max.value.valence?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
valence.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
valence: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
valence: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
valence: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.popularity),
|
||||
values: popularity.value,
|
||||
base: 100,
|
||||
values: (
|
||||
target: target.value.popularity?.toDouble() ?? 0,
|
||||
min: min.value.popularity?.toDouble() ?? 0,
|
||||
max: max.value.popularity?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
popularity.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
popularity: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
popularity: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
popularity: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeDials(
|
||||
title: Text(context.l10n.key),
|
||||
values: key.value,
|
||||
base: 11,
|
||||
values: (
|
||||
target: target.value.key?.toDouble() ?? 0,
|
||||
min: min.value.key?.toDouble() ?? 0,
|
||||
max: max.value.key?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
key.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
key: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
key: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
key: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeFields(
|
||||
title: Text(context.l10n.duration),
|
||||
values: (
|
||||
max: durationMs.value.max / 1000,
|
||||
target: durationMs.value.target / 1000,
|
||||
min: durationMs.value.min / 1000,
|
||||
max: (max.value.durationMs ?? 0) / 1000,
|
||||
target: (target.value.durationMs ?? 0) / 1000,
|
||||
min: (min.value.durationMs ?? 0) / 1000,
|
||||
),
|
||||
onChanged: (value) {
|
||||
durationMs.value = (
|
||||
max: value.max * 1000,
|
||||
target: value.target * 1000,
|
||||
min: value.min * 1000,
|
||||
target.value = target.value.copyWith(
|
||||
durationMs: (value.target * 1000).toInt(),
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
durationMs: (value.min * 1000).toInt(),
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
durationMs: (value.max * 1000).toInt(),
|
||||
);
|
||||
},
|
||||
presets: {
|
||||
@ -452,23 +563,59 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
),
|
||||
RecommendationAttributeFields(
|
||||
title: Text(context.l10n.tempo),
|
||||
values: tempo.value,
|
||||
values: (
|
||||
max: max.value.tempo?.toDouble() ?? 0,
|
||||
target: target.value.tempo?.toDouble() ?? 0,
|
||||
min: min.value.tempo?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
tempo.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
tempo: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
tempo: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
tempo: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeFields(
|
||||
title: Text(context.l10n.mode),
|
||||
values: mode.value,
|
||||
values: (
|
||||
max: max.value.mode?.toDouble() ?? 0,
|
||||
target: target.value.mode?.toDouble() ?? 0,
|
||||
min: min.value.mode?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
mode.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
mode: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
mode: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
mode: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
RecommendationAttributeFields(
|
||||
title: Text(context.l10n.time_signature),
|
||||
values: timeSignature.value,
|
||||
values: (
|
||||
max: max.value.timeSignature?.toDouble() ?? 0,
|
||||
target: target.value.timeSignature?.toDouble() ?? 0,
|
||||
min: min.value.timeSignature?.toDouble() ?? 0,
|
||||
),
|
||||
onChanged: (value) {
|
||||
timeSignature.value = value;
|
||||
target.value = target.value.copyWith(
|
||||
timeSignature: value.target,
|
||||
);
|
||||
min.value = min.value.copyWith(
|
||||
timeSignature: value.min,
|
||||
);
|
||||
max.value = max.value.copyWith(
|
||||
timeSignature: value.max,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
@ -480,35 +627,18 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
||||
genres.value.isEmpty
|
||||
? null
|
||||
: () {
|
||||
final PlaylistGenerateResultRouteState
|
||||
routeState = (
|
||||
seeds: (
|
||||
artists: artists.value
|
||||
.map((a) => a.id!)
|
||||
.toList(),
|
||||
tracks: tracks.value
|
||||
.map((t) => t.id!)
|
||||
.toList(),
|
||||
genres: genres.value
|
||||
),
|
||||
market: market.value,
|
||||
final routeState =
|
||||
GeneratePlaylistProviderInput(
|
||||
seedArtists: artists.value
|
||||
.map((a) => a.id!)
|
||||
.toList(),
|
||||
seedTracks:
|
||||
tracks.value.map((t) => t.id!).toList(),
|
||||
seedGenres: genres.value,
|
||||
limit: limit.value,
|
||||
parameters: (
|
||||
acousticness: acousticness.value,
|
||||
danceability: danceability.value,
|
||||
energy: energy.value,
|
||||
instrumentalness: instrumentalness.value,
|
||||
liveness: liveness.value,
|
||||
loudness: loudness.value,
|
||||
speechiness: speechiness.value,
|
||||
valence: valence.value,
|
||||
popularity: popularity.value,
|
||||
key: key.value,
|
||||
duration_ms: durationMs.value,
|
||||
tempo: tempo.value,
|
||||
mode: mode.value,
|
||||
time_signature: timeSignature.value,
|
||||
),
|
||||
max: max.value,
|
||||
min: min.value,
|
||||
target: target.value,
|
||||
);
|
||||
GoRouter.of(context).push(
|
||||
"/library/generate/result",
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -10,19 +9,12 @@ import 'package:spotube/components/playlist/playlist_create_dialog.dart';
|
||||
import 'package:spotube/components/shared/dialogs/playlist_add_track_dialog.dart';
|
||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/models/spotify/recommendation_seeds.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/queries/playlist.dart';
|
||||
import 'package:spotube/services/queries/queries.dart';
|
||||
|
||||
typedef PlaylistGenerateResultRouteState = ({
|
||||
({List<String> tracks, List<String> artists, List<String> genres})? seeds,
|
||||
RecommendationParameters? parameters,
|
||||
int limit,
|
||||
Market? market,
|
||||
});
|
||||
import 'package:spotube/provider/spotify/spotify.dart';
|
||||
|
||||
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
||||
final PlaylistGenerateResultRouteState state;
|
||||
final GeneratePlaylistProviderInput state;
|
||||
|
||||
const PlaylistGenerateResultPage({
|
||||
super.key,
|
||||
@ -34,225 +26,207 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
||||
final router = GoRouter.of(context);
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final (:seeds, :parameters, :limit, :market) = state;
|
||||
|
||||
final queryClient = useQueryClient();
|
||||
final generatedPlaylist = useQueries.playlist.generate(
|
||||
ref,
|
||||
seeds: seeds,
|
||||
parameters: parameters,
|
||||
limit: limit,
|
||||
market: market,
|
||||
);
|
||||
final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
|
||||
|
||||
final selectedTracks = useState<List<String>>(
|
||||
generatedPlaylist.data?.map((e) => e.id!).toList() ?? [],
|
||||
generatedPlaylist.value?.map((e) => e.id!).toList() ?? [],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (generatedPlaylist.data != null) {
|
||||
if (generatedPlaylist.value != null) {
|
||||
selectedTracks.value =
|
||||
generatedPlaylist.data!.map((e) => e.id!).toList();
|
||||
generatedPlaylist.value!.map((e) => e.id!).toList();
|
||||
}
|
||||
return null;
|
||||
}, [generatedPlaylist.data]);
|
||||
}, [generatedPlaylist.value]);
|
||||
|
||||
final isAllTrackSelected =
|
||||
selectedTracks.value.length == (generatedPlaylist.data?.length ?? 0);
|
||||
selectedTracks.value.length == (generatedPlaylist.value?.length ?? 0);
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
queryClient.cache.removeQuery(generatedPlaylist);
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||
body: generatedPlaylist.isLoading
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Text(context.l10n.generating_playlist),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
GridView(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
mainAxisExtent: 32,
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||
body: generatedPlaylist.isLoading
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Text(context.l10n.generating_playlist),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
children: [
|
||||
GridView(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
mainAxisExtent: 32,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.play),
|
||||
label: Text(context.l10n.play),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await playlistNotifier.load(
|
||||
generatedPlaylist.value!.where(
|
||||
(e) => selectedTracks.value.contains(e.id!),
|
||||
),
|
||||
autoPlay: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
shrinkWrap: true,
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
label: Text(context.l10n.add_to_queue),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await playlistNotifier.addTracks(
|
||||
generatedPlaylist.value!.where(
|
||||
(e) => selectedTracks.value.contains(e.id!),
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.add_count_to_queue(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.addFilled),
|
||||
label: Text(context.l10n.create_a_playlist),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final playlist = await showDialog<Playlist>(
|
||||
context: context,
|
||||
builder: (context) => PlaylistCreateDialog(
|
||||
trackIds: selectedTracks.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (playlist != null) {
|
||||
router.go(
|
||||
'/playlist/${playlist.id}',
|
||||
extra: playlist,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.playlistAdd),
|
||||
label: Text(context.l10n.add_to_playlist),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final hasAdded = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => PlaylistAddTrackDialog(
|
||||
openFromPlaylist: null,
|
||||
tracks: selectedTracks.value
|
||||
.map(
|
||||
(e) => generatedPlaylist.value!
|
||||
.firstWhere(
|
||||
(element) => element.id == e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted && hasAdded == true) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.add_count_to_playlist(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (generatedPlaylist.value != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.play),
|
||||
label: Text(context.l10n.play),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await playlistNotifier.load(
|
||||
generatedPlaylist.data!.where(
|
||||
(e) =>
|
||||
selectedTracks.value.contains(e.id!),
|
||||
),
|
||||
autoPlay: true,
|
||||
);
|
||||
},
|
||||
Text(
|
||||
context.l10n.selected_count_tracks(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.queueAdd),
|
||||
label: Text(context.l10n.add_to_queue),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
await playlistNotifier.addTracks(
|
||||
generatedPlaylist.data!.where(
|
||||
(e) =>
|
||||
selectedTracks.value.contains(e.id!),
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.add_count_to_queue(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (isAllTrackSelected) {
|
||||
selectedTracks.value = [];
|
||||
} else {
|
||||
selectedTracks.value = generatedPlaylist.value
|
||||
?.map((e) => e.id!)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.selectionCheck),
|
||||
label: Text(
|
||||
isAllTrackSelected
|
||||
? context.l10n.deselect_all
|
||||
: context.l10n.select_all,
|
||||
),
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.addFilled),
|
||||
label: Text(context.l10n.create_a_playlist),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final playlist = await showDialog<Playlist>(
|
||||
context: context,
|
||||
builder: (context) => PlaylistCreateDialog(
|
||||
trackIds: selectedTracks.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (playlist != null) {
|
||||
router.go(
|
||||
'/playlist/${playlist.id}',
|
||||
extra: playlist,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
FilledButton.tonalIcon(
|
||||
icon: const Icon(SpotubeIcons.playlistAdd),
|
||||
label: Text(context.l10n.add_to_playlist),
|
||||
onPressed: selectedTracks.value.isEmpty
|
||||
? null
|
||||
: () async {
|
||||
final hasAdded = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
PlaylistAddTrackDialog(
|
||||
openFromPlaylist: null,
|
||||
tracks: selectedTracks.value
|
||||
.map(
|
||||
(e) => generatedPlaylist.data!
|
||||
.firstWhere(
|
||||
(element) => element.id == e,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted && hasAdded == true) {
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.add_count_to_playlist(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (generatedPlaylist.data != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.selected_count_tracks(
|
||||
selectedTracks.value.length,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (isAllTrackSelected) {
|
||||
selectedTracks.value = [];
|
||||
} else {
|
||||
selectedTracks.value = generatedPlaylist.data
|
||||
?.map((e) => e.id!)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
},
|
||||
icon: const Icon(SpotubeIcons.selectionCheck),
|
||||
label: Text(
|
||||
isAllTrackSelected
|
||||
? context.l10n.deselect_all
|
||||
: context.l10n.select_all,
|
||||
),
|
||||
),
|
||||
for (final track in generatedPlaylist.value ?? [])
|
||||
CheckboxListTile(
|
||||
value: selectedTracks.value.contains(track.id),
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedTracks.value.add(track.id!);
|
||||
} else {
|
||||
selectedTracks.value.remove(track.id);
|
||||
}
|
||||
selectedTracks.value =
|
||||
selectedTracks.value.toList();
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: SimpleTrackTile(track: track),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: const EdgeInsets.all(0),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final track in generatedPlaylist.data ?? [])
|
||||
CheckboxListTile(
|
||||
value: selectedTracks.value.contains(track.id),
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
selectedTracks.value.add(track.id!);
|
||||
} else {
|
||||
selectedTracks.value.remove(track.id);
|
||||
}
|
||||
selectedTracks.value =
|
||||
selectedTracks.value.toList();
|
||||
},
|
||||
controlAffinity:
|
||||
ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
title: SimpleTrackTile(track: track),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,40 @@
|
||||
part of '../spotify.dart';
|
||||
|
||||
typedef GeneratePlaylistProviderInput = ({
|
||||
Iterable<String>? seedArtists,
|
||||
Iterable<String>? seedGenres,
|
||||
Iterable<String>? seedTracks,
|
||||
int limit,
|
||||
RecommendationSeeds? max,
|
||||
RecommendationSeeds? min,
|
||||
RecommendationSeeds? target,
|
||||
});
|
||||
|
||||
final generatePlaylistProvider =
|
||||
FutureProvider.family<List<TrackSimple>, GeneratePlaylistProviderInput>(
|
||||
final generatePlaylistProvider = FutureProvider.autoDispose
|
||||
.family<List<Track>, GeneratePlaylistProviderInput>(
|
||||
(ref, input) async {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final market = ref.watch(
|
||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||
);
|
||||
|
||||
final recommendation = await spotify.recommendations.get(
|
||||
final recommendation = await spotify.recommendations
|
||||
.get(
|
||||
limit: input.limit,
|
||||
seedArtists: input.seedArtists?.toList(),
|
||||
seedGenres: input.seedGenres?.toList(),
|
||||
seedTracks: input.seedTracks?.toList(),
|
||||
market: market,
|
||||
max: input.max?.toJson().cast<String, num>(),
|
||||
min: input.min?.toJson().cast<String, num>(),
|
||||
target: input.target?.toJson().cast<String, num>(),
|
||||
);
|
||||
max: (input.max?.toJson()?..removeWhere((key, value) => value == null))
|
||||
?.cast<String, num>(),
|
||||
min: (input.min?.toJson()?..removeWhere((key, value) => value == null))
|
||||
?.cast<String, num>(),
|
||||
target: (input.target?.toJson()
|
||||
?..removeWhere((key, value) => value == null))
|
||||
?.cast<String, num>(),
|
||||
)
|
||||
.catchError((e, stackTrace) {
|
||||
Catcher2.reportCheckedError(e, stackTrace);
|
||||
return Recommendations();
|
||||
});
|
||||
|
||||
return recommendation.tracks?.toList() ?? [];
|
||||
if (recommendation.tracks?.isEmpty ?? true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final tracks = await spotify.tracks
|
||||
.list(recommendation.tracks!.map((e) => e.id!).toList());
|
||||
|
||||
return tracks.toList();
|
||||
},
|
||||
);
|
||||
|
||||
@ -3,6 +3,7 @@ library spotify;
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user