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:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart' hide Search;
|
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/album/album.dart';
|
||||||
import 'package:spotube/pages/getting_started/getting_started.dart';
|
import 'package:spotube/pages/getting_started/getting_started.dart';
|
||||||
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
import 'package:spotube/pages/home/genres/genre_playlists.dart';
|
||||||
@ -96,8 +97,7 @@ final routerProvider = Provider((ref) {
|
|||||||
path: "result",
|
path: "result",
|
||||||
pageBuilder: (context, state) => SpotubePage(
|
pageBuilder: (context, state) => SpotubePage(
|
||||||
child: PlaylistGenerateResultPage(
|
child: PlaylistGenerateResultPage(
|
||||||
state:
|
state: state.extra as GeneratePlaylistProviderInput,
|
||||||
state.extra as PlaylistGenerateResultRouteState,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -3,9 +3,22 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'recommendation_seeds.freezed.dart';
|
part 'recommendation_seeds.freezed.dart';
|
||||||
part 'recommendation_seeds.g.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
|
@freezed
|
||||||
class RecommendationSeeds with _$RecommendationSeeds {
|
class RecommendationSeeds with _$RecommendationSeeds {
|
||||||
factory RecommendationSeeds(
|
factory RecommendationSeeds({
|
||||||
num? acousticness,
|
num? acousticness,
|
||||||
num? danceability,
|
num? danceability,
|
||||||
@JsonKey(name: "duration_ms") num? durationMs,
|
@JsonKey(name: "duration_ms") num? durationMs,
|
||||||
@ -20,7 +33,7 @@ class RecommendationSeeds with _$RecommendationSeeds {
|
|||||||
num? tempo,
|
num? tempo,
|
||||||
@JsonKey(name: "time_signature") num? timeSignature,
|
@JsonKey(name: "time_signature") num? timeSignature,
|
||||||
num? valence,
|
num? valence,
|
||||||
) = _RecommendationSeeds;
|
}) = _RecommendationSeeds;
|
||||||
|
|
||||||
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
|
factory RecommendationSeeds.fromJson(Map<String, dynamic> json) =>
|
||||||
_$RecommendationSeedsFromJson(json);
|
_$RecommendationSeedsFromJson(json);
|
||||||
|
|||||||
@ -14,6 +14,316 @@ T _$identity<T>(T value) => value;
|
|||||||
final _privateConstructorUsedError = UnsupportedError(
|
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');
|
'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) {
|
RecommendationSeeds _$RecommendationSeedsFromJson(Map<String, dynamic> json) {
|
||||||
return _RecommendationSeeds.fromJson(json);
|
return _RecommendationSeeds.fromJson(json);
|
||||||
}
|
}
|
||||||
@ -207,59 +517,59 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
|
|||||||
Object? valence = freezed,
|
Object? valence = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$RecommendationSeedsImpl(
|
return _then(_$RecommendationSeedsImpl(
|
||||||
freezed == acousticness
|
acousticness: freezed == acousticness
|
||||||
? _value.acousticness
|
? _value.acousticness
|
||||||
: acousticness // ignore: cast_nullable_to_non_nullable
|
: acousticness // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == danceability
|
danceability: freezed == danceability
|
||||||
? _value.danceability
|
? _value.danceability
|
||||||
: danceability // ignore: cast_nullable_to_non_nullable
|
: danceability // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == durationMs
|
durationMs: freezed == durationMs
|
||||||
? _value.durationMs
|
? _value.durationMs
|
||||||
: durationMs // ignore: cast_nullable_to_non_nullable
|
: durationMs // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == energy
|
energy: freezed == energy
|
||||||
? _value.energy
|
? _value.energy
|
||||||
: energy // ignore: cast_nullable_to_non_nullable
|
: energy // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == instrumentalness
|
instrumentalness: freezed == instrumentalness
|
||||||
? _value.instrumentalness
|
? _value.instrumentalness
|
||||||
: instrumentalness // ignore: cast_nullable_to_non_nullable
|
: instrumentalness // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == key
|
key: freezed == key
|
||||||
? _value.key
|
? _value.key
|
||||||
: key // ignore: cast_nullable_to_non_nullable
|
: key // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == liveness
|
liveness: freezed == liveness
|
||||||
? _value.liveness
|
? _value.liveness
|
||||||
: liveness // ignore: cast_nullable_to_non_nullable
|
: liveness // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == loudness
|
loudness: freezed == loudness
|
||||||
? _value.loudness
|
? _value.loudness
|
||||||
: loudness // ignore: cast_nullable_to_non_nullable
|
: loudness // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == mode
|
mode: freezed == mode
|
||||||
? _value.mode
|
? _value.mode
|
||||||
: mode // ignore: cast_nullable_to_non_nullable
|
: mode // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == popularity
|
popularity: freezed == popularity
|
||||||
? _value.popularity
|
? _value.popularity
|
||||||
: popularity // ignore: cast_nullable_to_non_nullable
|
: popularity // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == speechiness
|
speechiness: freezed == speechiness
|
||||||
? _value.speechiness
|
? _value.speechiness
|
||||||
: speechiness // ignore: cast_nullable_to_non_nullable
|
: speechiness // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == tempo
|
tempo: freezed == tempo
|
||||||
? _value.tempo
|
? _value.tempo
|
||||||
: tempo // ignore: cast_nullable_to_non_nullable
|
: tempo // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == timeSignature
|
timeSignature: freezed == timeSignature
|
||||||
? _value.timeSignature
|
? _value.timeSignature
|
||||||
: timeSignature // ignore: cast_nullable_to_non_nullable
|
: timeSignature // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
freezed == valence
|
valence: freezed == valence
|
||||||
? _value.valence
|
? _value.valence
|
||||||
: valence // ignore: cast_nullable_to_non_nullable
|
: valence // ignore: cast_nullable_to_non_nullable
|
||||||
as num?,
|
as num?,
|
||||||
@ -271,7 +581,7 @@ class __$$RecommendationSeedsImplCopyWithImpl<$Res>
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
||||||
_$RecommendationSeedsImpl(
|
_$RecommendationSeedsImpl(
|
||||||
this.acousticness,
|
{this.acousticness,
|
||||||
this.danceability,
|
this.danceability,
|
||||||
@JsonKey(name: "duration_ms") this.durationMs,
|
@JsonKey(name: "duration_ms") this.durationMs,
|
||||||
this.energy,
|
this.energy,
|
||||||
@ -284,7 +594,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
|||||||
this.speechiness,
|
this.speechiness,
|
||||||
this.tempo,
|
this.tempo,
|
||||||
@JsonKey(name: "time_signature") this.timeSignature,
|
@JsonKey(name: "time_signature") this.timeSignature,
|
||||||
this.valence);
|
this.valence});
|
||||||
|
|
||||||
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$RecommendationSeedsImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$RecommendationSeedsImplFromJson(json);
|
_$$RecommendationSeedsImplFromJson(json);
|
||||||
@ -391,7 +701,7 @@ class _$RecommendationSeedsImpl implements _RecommendationSeeds {
|
|||||||
|
|
||||||
abstract class _RecommendationSeeds implements RecommendationSeeds {
|
abstract class _RecommendationSeeds implements RecommendationSeeds {
|
||||||
factory _RecommendationSeeds(
|
factory _RecommendationSeeds(
|
||||||
final num? acousticness,
|
{final num? acousticness,
|
||||||
final num? danceability,
|
final num? danceability,
|
||||||
@JsonKey(name: "duration_ms") final num? durationMs,
|
@JsonKey(name: "duration_ms") final num? durationMs,
|
||||||
final num? energy,
|
final num? energy,
|
||||||
@ -404,7 +714,7 @@ abstract class _RecommendationSeeds implements RecommendationSeeds {
|
|||||||
final num? speechiness,
|
final num? speechiness,
|
||||||
final num? tempo,
|
final num? tempo,
|
||||||
@JsonKey(name: "time_signature") final num? timeSignature,
|
@JsonKey(name: "time_signature") final num? timeSignature,
|
||||||
final num? valence) = _$RecommendationSeedsImpl;
|
final num? valence}) = _$RecommendationSeedsImpl;
|
||||||
|
|
||||||
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
|
factory _RecommendationSeeds.fromJson(Map<String, dynamic> json) =
|
||||||
_$RecommendationSeedsImpl.fromJson;
|
_$RecommendationSeedsImpl.fromJson;
|
||||||
|
|||||||
@ -9,20 +9,20 @@ part of 'recommendation_seeds.dart';
|
|||||||
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
|
_$RecommendationSeedsImpl _$$RecommendationSeedsImplFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
_$RecommendationSeedsImpl(
|
_$RecommendationSeedsImpl(
|
||||||
json['acousticness'] as num?,
|
acousticness: json['acousticness'] as num?,
|
||||||
json['danceability'] as num?,
|
danceability: json['danceability'] as num?,
|
||||||
json['duration_ms'] as num?,
|
durationMs: json['duration_ms'] as num?,
|
||||||
json['energy'] as num?,
|
energy: json['energy'] as num?,
|
||||||
json['instrumentalness'] as num?,
|
instrumentalness: json['instrumentalness'] as num?,
|
||||||
json['key'] as num?,
|
key: json['key'] as num?,
|
||||||
json['liveness'] as num?,
|
liveness: json['liveness'] as num?,
|
||||||
json['loudness'] as num?,
|
loudness: json['loudness'] as num?,
|
||||||
json['mode'] as num?,
|
mode: json['mode'] as num?,
|
||||||
json['popularity'] as num?,
|
popularity: json['popularity'] as num?,
|
||||||
json['speechiness'] as num?,
|
speechiness: json['speechiness'] as num?,
|
||||||
json['tempo'] as num?,
|
tempo: json['tempo'] as num?,
|
||||||
json['time_signature'] as num?,
|
timeSignature: json['time_signature'] as num?,
|
||||||
json['valence'] as num?,
|
valence: json['valence'] as num?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$RecommendationSeedsImplToJson(
|
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/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/constrains.dart';
|
import 'package:spotube/extensions/constrains.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/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/spotify_provider.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/queries/playlist.dart';
|
|
||||||
import 'package:spotube/services/queries/queries.dart';
|
|
||||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
|
||||||
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
|
const RecommendationAttribute zeroValues = (min: 0, target: 0, max: 0);
|
||||||
@ -35,7 +34,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
final textTheme = theme.textTheme;
|
final textTheme = theme.textTheme;
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
|
|
||||||
final genresCollection = useQueries.category.genreSeeds(ref);
|
final genresCollection = ref.watch(categoryGenresProvider);
|
||||||
|
|
||||||
final limit = useValueNotifier<int>(10);
|
final limit = useValueNotifier<int>(10);
|
||||||
final market = useValueNotifier<Market>(preferences.recommendationMarket);
|
final market = useValueNotifier<Market>(preferences.recommendationMarket);
|
||||||
@ -51,22 +50,9 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
5 - genres.value.length - artists.value.length - tracks.value.length;
|
5 - genres.value.length - artists.value.length - tracks.value.length;
|
||||||
|
|
||||||
// Dial (int 0-1) attributes
|
// Dial (int 0-1) attributes
|
||||||
final acousticness = useState<RecommendationAttribute>(zeroValues);
|
final min = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||||
final danceability = useState<RecommendationAttribute>(zeroValues);
|
final max = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||||
final energy = useState<RecommendationAttribute>(zeroValues);
|
final target = useState<RecommendationSeeds>(RecommendationSeeds());
|
||||||
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 artistAutoComplete = SeedsMultiAutocomplete<Artist>(
|
final artistAutoComplete = SeedsMultiAutocomplete<Artist>(
|
||||||
seeds: artists,
|
seeds: artists,
|
||||||
@ -204,7 +190,7 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final genreSelector = MultiSelectField<String>(
|
final genreSelector = MultiSelectField<String>(
|
||||||
options: genresCollection.data ?? [],
|
options: genresCollection.value ?? [],
|
||||||
selectedOptions: genres.value,
|
selectedOptions: genres.value,
|
||||||
getValueForOption: (option) => option,
|
getValueForOption: (option) => option,
|
||||||
onSelected: (value) {
|
onSelected: (value) {
|
||||||
@ -356,88 +342,213 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
RecommendationAttributeDials(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.acousticness),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.danceability),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.energy),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.instrumentalness),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.liveness),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.loudness),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.speechiness),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.valence),
|
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) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.popularity),
|
title: Text(context.l10n.popularity),
|
||||||
values: popularity.value,
|
|
||||||
base: 100,
|
base: 100,
|
||||||
|
values: (
|
||||||
|
target: target.value.popularity?.toDouble() ?? 0,
|
||||||
|
min: min.value.popularity?.toDouble() ?? 0,
|
||||||
|
max: max.value.popularity?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
onChanged: (value) {
|
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(
|
RecommendationAttributeDials(
|
||||||
title: Text(context.l10n.key),
|
title: Text(context.l10n.key),
|
||||||
values: key.value,
|
|
||||||
base: 11,
|
base: 11,
|
||||||
|
values: (
|
||||||
|
target: target.value.key?.toDouble() ?? 0,
|
||||||
|
min: min.value.key?.toDouble() ?? 0,
|
||||||
|
max: max.value.key?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
onChanged: (value) {
|
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(
|
RecommendationAttributeFields(
|
||||||
title: Text(context.l10n.duration),
|
title: Text(context.l10n.duration),
|
||||||
values: (
|
values: (
|
||||||
max: durationMs.value.max / 1000,
|
max: (max.value.durationMs ?? 0) / 1000,
|
||||||
target: durationMs.value.target / 1000,
|
target: (target.value.durationMs ?? 0) / 1000,
|
||||||
min: durationMs.value.min / 1000,
|
min: (min.value.durationMs ?? 0) / 1000,
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
durationMs.value = (
|
target.value = target.value.copyWith(
|
||||||
max: value.max * 1000,
|
durationMs: (value.target * 1000).toInt(),
|
||||||
target: value.target * 1000,
|
);
|
||||||
min: value.min * 1000,
|
min.value = min.value.copyWith(
|
||||||
|
durationMs: (value.min * 1000).toInt(),
|
||||||
|
);
|
||||||
|
max.value = max.value.copyWith(
|
||||||
|
durationMs: (value.max * 1000).toInt(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
presets: {
|
presets: {
|
||||||
@ -452,23 +563,59 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
RecommendationAttributeFields(
|
RecommendationAttributeFields(
|
||||||
title: Text(context.l10n.tempo),
|
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) {
|
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(
|
RecommendationAttributeFields(
|
||||||
title: Text(context.l10n.mode),
|
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) {
|
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(
|
RecommendationAttributeFields(
|
||||||
title: Text(context.l10n.time_signature),
|
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) {
|
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),
|
const SizedBox(height: 20),
|
||||||
@ -480,35 +627,18 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
|
|||||||
genres.value.isEmpty
|
genres.value.isEmpty
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
final PlaylistGenerateResultRouteState
|
final routeState =
|
||||||
routeState = (
|
GeneratePlaylistProviderInput(
|
||||||
seeds: (
|
seedArtists: artists.value
|
||||||
artists: artists.value
|
.map((a) => a.id!)
|
||||||
.map((a) => a.id!)
|
.toList(),
|
||||||
.toList(),
|
seedTracks:
|
||||||
tracks: tracks.value
|
tracks.value.map((t) => t.id!).toList(),
|
||||||
.map((t) => t.id!)
|
seedGenres: genres.value,
|
||||||
.toList(),
|
|
||||||
genres: genres.value
|
|
||||||
),
|
|
||||||
market: market.value,
|
|
||||||
limit: limit.value,
|
limit: limit.value,
|
||||||
parameters: (
|
max: max.value,
|
||||||
acousticness: acousticness.value,
|
min: min.value,
|
||||||
danceability: danceability.value,
|
target: target.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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
GoRouter.of(context).push(
|
GoRouter.of(context).push(
|
||||||
"/library/generate/result",
|
"/library/generate/result",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import 'package:fl_query_hooks/fl_query_hooks.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:go_router/go_router.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/dialogs/playlist_add_track_dialog.dart';
|
||||||
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
import 'package:spotube/components/shared/page_window_title_bar.dart';
|
||||||
import 'package:spotube/extensions/context.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/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
import 'package:spotube/services/queries/playlist.dart';
|
import 'package:spotube/provider/spotify/spotify.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,
|
|
||||||
});
|
|
||||||
|
|
||||||
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
class PlaylistGenerateResultPage extends HookConsumerWidget {
|
||||||
final PlaylistGenerateResultRouteState state;
|
final GeneratePlaylistProviderInput state;
|
||||||
|
|
||||||
const PlaylistGenerateResultPage({
|
const PlaylistGenerateResultPage({
|
||||||
super.key,
|
super.key,
|
||||||
@ -34,225 +26,207 @@ class PlaylistGenerateResultPage extends HookConsumerWidget {
|
|||||||
final router = GoRouter.of(context);
|
final router = GoRouter.of(context);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||||
final (:seeds, :parameters, :limit, :market) = state;
|
|
||||||
|
|
||||||
final queryClient = useQueryClient();
|
final generatedPlaylist = ref.watch(generatePlaylistProvider(state));
|
||||||
final generatedPlaylist = useQueries.playlist.generate(
|
|
||||||
ref,
|
|
||||||
seeds: seeds,
|
|
||||||
parameters: parameters,
|
|
||||||
limit: limit,
|
|
||||||
market: market,
|
|
||||||
);
|
|
||||||
|
|
||||||
final selectedTracks = useState<List<String>>(
|
final selectedTracks = useState<List<String>>(
|
||||||
generatedPlaylist.data?.map((e) => e.id!).toList() ?? [],
|
generatedPlaylist.value?.map((e) => e.id!).toList() ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (generatedPlaylist.data != null) {
|
if (generatedPlaylist.value != null) {
|
||||||
selectedTracks.value =
|
selectedTracks.value =
|
||||||
generatedPlaylist.data!.map((e) => e.id!).toList();
|
generatedPlaylist.value!.map((e) => e.id!).toList();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [generatedPlaylist.data]);
|
}, [generatedPlaylist.value]);
|
||||||
|
|
||||||
final isAllTrackSelected =
|
final isAllTrackSelected =
|
||||||
selectedTracks.value.length == (generatedPlaylist.data?.length ?? 0);
|
selectedTracks.value.length == (generatedPlaylist.value?.length ?? 0);
|
||||||
|
|
||||||
return WillPopScope(
|
return Scaffold(
|
||||||
onWillPop: () async {
|
appBar: const PageWindowTitleBar(leading: BackButton()),
|
||||||
queryClient.cache.removeQuery(generatedPlaylist);
|
body: generatedPlaylist.isLoading
|
||||||
return true;
|
? Center(
|
||||||
},
|
child: Column(
|
||||||
child: Scaffold(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
appBar: const PageWindowTitleBar(leading: BackButton()),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
body: generatedPlaylist.isLoading
|
children: [
|
||||||
? Center(
|
const CircularProgressIndicator(),
|
||||||
child: Column(
|
Text(context.l10n.generating_playlist),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
),
|
||||||
children: [
|
)
|
||||||
const CircularProgressIndicator(),
|
: Padding(
|
||||||
Text(context.l10n.generating_playlist),
|
padding: const EdgeInsets.all(8.0),
|
||||||
],
|
child: ListView(
|
||||||
),
|
children: [
|
||||||
)
|
GridView(
|
||||||
: Padding(
|
gridDelegate:
|
||||||
padding: const EdgeInsets.all(8.0),
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
child: ListView(
|
crossAxisCount: 2,
|
||||||
children: [
|
crossAxisSpacing: 8,
|
||||||
GridView(
|
mainAxisSpacing: 8,
|
||||||
gridDelegate:
|
mainAxisExtent: 32,
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
),
|
||||||
crossAxisCount: 2,
|
shrinkWrap: true,
|
||||||
crossAxisSpacing: 8,
|
children: [
|
||||||
mainAxisSpacing: 8,
|
FilledButton.tonalIcon(
|
||||||
mainAxisExtent: 32,
|
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: [
|
children: [
|
||||||
FilledButton.tonalIcon(
|
Text(
|
||||||
icon: const Icon(SpotubeIcons.play),
|
context.l10n.selected_count_tracks(
|
||||||
label: Text(context.l10n.play),
|
selectedTracks.value.length,
|
||||||
onPressed: selectedTracks.value.isEmpty
|
),
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
await playlistNotifier.load(
|
|
||||||
generatedPlaylist.data!.where(
|
|
||||||
(e) =>
|
|
||||||
selectedTracks.value.contains(e.id!),
|
|
||||||
),
|
|
||||||
autoPlay: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
FilledButton.tonalIcon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(SpotubeIcons.queueAdd),
|
onPressed: () {
|
||||||
label: Text(context.l10n.add_to_queue),
|
if (isAllTrackSelected) {
|
||||||
onPressed: selectedTracks.value.isEmpty
|
selectedTracks.value = [];
|
||||||
? null
|
} else {
|
||||||
: () async {
|
selectedTracks.value = generatedPlaylist.value
|
||||||
await playlistNotifier.addTracks(
|
?.map((e) => e.id!)
|
||||||
generatedPlaylist.data!.where(
|
.toList() ??
|
||||||
(e) =>
|
[];
|
||||||
selectedTracks.value.contains(e.id!),
|
}
|
||||||
),
|
},
|
||||||
);
|
icon: const Icon(SpotubeIcons.selectionCheck),
|
||||||
if (context.mounted) {
|
label: Text(
|
||||||
scaffoldMessenger.showSnackBar(
|
isAllTrackSelected
|
||||||
SnackBar(
|
? context.l10n.deselect_all
|
||||||
content: Text(
|
: context.l10n.select_all,
|
||||||
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.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),
|
const SizedBox(height: 8),
|
||||||
if (generatedPlaylist.data != null)
|
Card(
|
||||||
Row(
|
margin: const EdgeInsets.all(0),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
for (final track in generatedPlaylist.value ?? [])
|
||||||
context.l10n.selected_count_tracks(
|
CheckboxListTile(
|
||||||
selectedTracks.value.length,
|
value: selectedTracks.value.contains(track.id),
|
||||||
),
|
onChanged: (value) {
|
||||||
),
|
if (value == true) {
|
||||||
ElevatedButton.icon(
|
selectedTracks.value.add(track.id!);
|
||||||
onPressed: () {
|
} else {
|
||||||
if (isAllTrackSelected) {
|
selectedTracks.value.remove(track.id);
|
||||||
selectedTracks.value = [];
|
}
|
||||||
} else {
|
selectedTracks.value =
|
||||||
selectedTracks.value = generatedPlaylist.data
|
selectedTracks.value.toList();
|
||||||
?.map((e) => e.id!)
|
},
|
||||||
.toList() ??
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
[];
|
contentPadding: EdgeInsets.zero,
|
||||||
}
|
dense: true,
|
||||||
},
|
title: SimpleTrackTile(track: track),
|
||||||
icon: const Icon(SpotubeIcons.selectionCheck),
|
)
|
||||||
label: Text(
|
|
||||||
isAllTrackSelected
|
|
||||||
? context.l10n.deselect_all
|
|
||||||
: context.l10n.select_all,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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';
|
part of '../spotify.dart';
|
||||||
|
|
||||||
typedef GeneratePlaylistProviderInput = ({
|
final generatePlaylistProvider = FutureProvider.autoDispose
|
||||||
Iterable<String>? seedArtists,
|
.family<List<Track>, GeneratePlaylistProviderInput>(
|
||||||
Iterable<String>? seedGenres,
|
|
||||||
Iterable<String>? seedTracks,
|
|
||||||
int limit,
|
|
||||||
RecommendationSeeds? max,
|
|
||||||
RecommendationSeeds? min,
|
|
||||||
RecommendationSeeds? target,
|
|
||||||
});
|
|
||||||
|
|
||||||
final generatePlaylistProvider =
|
|
||||||
FutureProvider.family<List<TrackSimple>, GeneratePlaylistProviderInput>(
|
|
||||||
(ref, input) async {
|
(ref, input) async {
|
||||||
final spotify = ref.watch(spotifyProvider);
|
final spotify = ref.watch(spotifyProvider);
|
||||||
final market = ref.watch(
|
final market = ref.watch(
|
||||||
userPreferencesProvider.select((s) => s.recommendationMarket),
|
userPreferencesProvider.select((s) => s.recommendationMarket),
|
||||||
);
|
);
|
||||||
|
|
||||||
final recommendation = await spotify.recommendations.get(
|
final recommendation = await spotify.recommendations
|
||||||
|
.get(
|
||||||
limit: input.limit,
|
limit: input.limit,
|
||||||
seedArtists: input.seedArtists?.toList(),
|
seedArtists: input.seedArtists?.toList(),
|
||||||
seedGenres: input.seedGenres?.toList(),
|
seedGenres: input.seedGenres?.toList(),
|
||||||
seedTracks: input.seedTracks?.toList(),
|
seedTracks: input.seedTracks?.toList(),
|
||||||
market: market,
|
market: market,
|
||||||
max: input.max?.toJson().cast<String, num>(),
|
max: (input.max?.toJson()?..removeWhere((key, value) => value == null))
|
||||||
min: input.min?.toJson().cast<String, num>(),
|
?.cast<String, num>(),
|
||||||
target: input.target?.toJson().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:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user