refactor(blacklist): use drift sql db instead of hive

This commit is contained in:
Kingkor Roy Tirtho 2024-06-14 22:23:12 +06:00
parent 52d4f60ccc
commit bf6cec8d69
9 changed files with 512 additions and 122 deletions

View File

@ -18,6 +18,7 @@ import 'package:spotube/components/links/artist_link.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/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
@ -170,11 +171,8 @@ class TrackOptions extends HookConsumerWidget {
final favorites = useTrackToggleLike(track, ref); final favorites = useTrackToggleLike(track, ref);
final isBlackListed = useMemoized( final isBlackListed = useMemoized(
() => blacklist.contains( () => blacklist.asData?.value.any(
BlacklistedElement.track( (element) => element.elementId == track.id,
track.id!,
track.name!,
),
), ),
[blacklist, track], [blacklist, track],
); );
@ -258,13 +256,16 @@ class TrackOptions extends HookConsumerWidget {
.removeTracks(playlistId ?? "", [track.id!]); .removeTracks(playlistId ?? "", [track.id!]);
break; break;
case TrackOptionValue.blacklist: case TrackOptionValue.blacklist:
if (isBlackListed) { if (isBlackListed == null) break;
ref.read(blacklistProvider.notifier).remove( if (isBlackListed == true) {
BlacklistedElement.track(track.id!, track.name!), await ref.read(blacklistProvider.notifier).remove(track.id!);
);
} else { } else {
ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistedElement.track(track.id!, track.name!), BlacklistTableCompanion.insert(
name: track.name!,
elementId: track.id!,
elementType: BlacklistedType.track,
),
); );
} }
break; break;
@ -399,10 +400,10 @@ class TrackOptions extends HookConsumerWidget {
PopSheetEntry( PopSheetEntry(
value: TrackOptionValue.blacklist, value: TrackOptionValue.blacklist,
leading: const Icon(SpotubeIcons.playlistRemove), leading: const Icon(SpotubeIcons.playlistRemove),
iconColor: !isBlackListed ? Colors.red[400] : null, iconColor: isBlackListed != true ? Colors.red[400] : null,
textColor: !isBlackListed ? Colors.red[400] : null, textColor: isBlackListed != true ? Colors.red[400] : null,
title: Text( title: Text(
isBlackListed isBlackListed == true
? context.l10n.remove_from_blacklist ? context.l10n.remove_from_blacklist
: context.l10n.add_to_blacklist, : context.l10n.add_to_blacklist,
), ),

View File

@ -53,14 +53,10 @@ class TrackTile extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final blacklist = ref.watch(blacklistProvider); final blacklist = ref.watch(blacklistProvider);
final blacklistNotifier = ref.watch(blacklistProvider.notifier);
final isBlackListed = useMemoized( final isBlackListed = useMemoized(
() => blacklist.contains( () => blacklistNotifier.contains(track),
BlacklistedElement.track(
track.id!,
track.name!,
),
),
[blacklist, track], [blacklist, track],
); );

View File

@ -19,12 +19,20 @@ part 'database.g.dart';
part 'tables/preferences.dart'; part 'tables/preferences.dart';
part 'tables/source_match.dart'; part 'tables/source_match.dart';
part 'tables/skip_segment.dart'; part 'tables/skip_segment.dart';
part 'tables/blacklist.dart';
part 'typeconverters/color.dart'; part 'typeconverters/color.dart';
part 'typeconverters/locale.dart'; part 'typeconverters/locale.dart';
part 'typeconverters/string_list.dart'; part 'typeconverters/string_list.dart';
@DriftDatabase(tables: [PreferencesTable, SourceMatchTable, SkipSegmentTable]) @DriftDatabase(
tables: [
PreferencesTable,
SourceMatchTable,
SkipSegmentTable,
BlacklistTable,
],
)
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());

View File

@ -1789,6 +1789,266 @@ class SkipSegmentTableCompanion extends UpdateCompanion<SkipSegmentTableData> {
} }
} }
class $BlacklistTableTable extends BlacklistTable
with TableInfo<$BlacklistTableTable, BlacklistTableData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$BlacklistTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _elementTypeMeta =
const VerificationMeta('elementType');
@override
late final GeneratedColumnWithTypeConverter<BlacklistedType, String>
elementType = GeneratedColumn<String>('element_type', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<BlacklistedType>(
$BlacklistTableTable.$converterelementType);
static const VerificationMeta _elementIdMeta =
const VerificationMeta('elementId');
@override
late final GeneratedColumn<String> elementId = GeneratedColumn<String>(
'element_id', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [id, name, elementType, elementId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'blacklist_table';
@override
VerificationContext validateIntegrity(Insertable<BlacklistTableData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
context.handle(_elementTypeMeta, const VerificationResult.success());
if (data.containsKey('element_id')) {
context.handle(_elementIdMeta,
elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta));
} else if (isInserting) {
context.missing(_elementIdMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
BlacklistTableData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return BlacklistTableData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name'])!,
elementType: $BlacklistTableTable.$converterelementType.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}element_type'])!),
elementId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}element_id'])!,
);
}
@override
$BlacklistTableTable createAlias(String alias) {
return $BlacklistTableTable(attachedDatabase, alias);
}
static JsonTypeConverter2<BlacklistedType, String, String>
$converterelementType =
const EnumNameConverter<BlacklistedType>(BlacklistedType.values);
}
class BlacklistTableData extends DataClass
implements Insertable<BlacklistTableData> {
final int id;
final String name;
final BlacklistedType elementType;
final String elementId;
const BlacklistTableData(
{required this.id,
required this.name,
required this.elementType,
required this.elementId});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['name'] = Variable<String>(name);
{
map['element_type'] = Variable<String>(
$BlacklistTableTable.$converterelementType.toSql(elementType));
}
map['element_id'] = Variable<String>(elementId);
return map;
}
BlacklistTableCompanion toCompanion(bool nullToAbsent) {
return BlacklistTableCompanion(
id: Value(id),
name: Value(name),
elementType: Value(elementType),
elementId: Value(elementId),
);
}
factory BlacklistTableData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return BlacklistTableData(
id: serializer.fromJson<int>(json['id']),
name: serializer.fromJson<String>(json['name']),
elementType: $BlacklistTableTable.$converterelementType
.fromJson(serializer.fromJson<String>(json['elementType'])),
elementId: serializer.fromJson<String>(json['elementId']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'name': serializer.toJson<String>(name),
'elementType': serializer.toJson<String>(
$BlacklistTableTable.$converterelementType.toJson(elementType)),
'elementId': serializer.toJson<String>(elementId),
};
}
BlacklistTableData copyWith(
{int? id,
String? name,
BlacklistedType? elementType,
String? elementId}) =>
BlacklistTableData(
id: id ?? this.id,
name: name ?? this.name,
elementType: elementType ?? this.elementType,
elementId: elementId ?? this.elementId,
);
@override
String toString() {
return (StringBuffer('BlacklistTableData(')
..write('id: $id, ')
..write('name: $name, ')
..write('elementType: $elementType, ')
..write('elementId: $elementId')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name, elementType, elementId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BlacklistTableData &&
other.id == this.id &&
other.name == this.name &&
other.elementType == this.elementType &&
other.elementId == this.elementId);
}
class BlacklistTableCompanion extends UpdateCompanion<BlacklistTableData> {
final Value<int> id;
final Value<String> name;
final Value<BlacklistedType> elementType;
final Value<String> elementId;
const BlacklistTableCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.elementType = const Value.absent(),
this.elementId = const Value.absent(),
});
BlacklistTableCompanion.insert({
this.id = const Value.absent(),
required String name,
required BlacklistedType elementType,
required String elementId,
}) : name = Value(name),
elementType = Value(elementType),
elementId = Value(elementId);
static Insertable<BlacklistTableData> custom({
Expression<int>? id,
Expression<String>? name,
Expression<String>? elementType,
Expression<String>? elementId,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (elementType != null) 'element_type': elementType,
if (elementId != null) 'element_id': elementId,
});
}
BlacklistTableCompanion copyWith(
{Value<int>? id,
Value<String>? name,
Value<BlacklistedType>? elementType,
Value<String>? elementId}) {
return BlacklistTableCompanion(
id: id ?? this.id,
name: name ?? this.name,
elementType: elementType ?? this.elementType,
elementId: elementId ?? this.elementId,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (elementType.present) {
map['element_type'] = Variable<String>(
$BlacklistTableTable.$converterelementType.toSql(elementType.value));
}
if (elementId.present) {
map['element_id'] = Variable<String>(elementId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('BlacklistTableCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('elementType: $elementType, ')
..write('elementId: $elementId')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase { abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e); _$AppDatabase(QueryExecutor e) : super(e);
_$AppDatabaseManager get managers => _$AppDatabaseManager(this); _$AppDatabaseManager get managers => _$AppDatabaseManager(this);
@ -1798,14 +2058,23 @@ abstract class _$AppDatabase extends GeneratedDatabase {
$SourceMatchTableTable(this); $SourceMatchTableTable(this);
late final $SkipSegmentTableTable skipSegmentTable = late final $SkipSegmentTableTable skipSegmentTable =
$SkipSegmentTableTable(this); $SkipSegmentTableTable(this);
late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this);
late final Index uniqTrackMatch = Index('uniq_track_match', late final Index uniqTrackMatch = Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
late final Index uniqueBlacklist = Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
@override @override
Iterable<TableInfo<Table, Object?>> get allTables => Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>(); allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override @override
List<DatabaseSchemaEntity> get allSchemaEntities => List<DatabaseSchemaEntity> get allSchemaEntities => [
[preferencesTable, sourceMatchTable, skipSegmentTable, uniqTrackMatch]; preferencesTable,
sourceMatchTable,
skipSegmentTable,
blacklistTable,
uniqTrackMatch,
uniqueBlacklist
];
} }
typedef $$PreferencesTableTableInsertCompanionBuilder typedef $$PreferencesTableTableInsertCompanionBuilder
@ -2571,6 +2840,130 @@ class $$SkipSegmentTableTableOrderingComposer
ColumnOrderings(column, joinBuilders: joinBuilders)); ColumnOrderings(column, joinBuilders: joinBuilders));
} }
typedef $$BlacklistTableTableInsertCompanionBuilder = BlacklistTableCompanion
Function({
Value<int> id,
required String name,
required BlacklistedType elementType,
required String elementId,
});
typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion
Function({
Value<int> id,
Value<String> name,
Value<BlacklistedType> elementType,
Value<String> elementId,
});
class $$BlacklistTableTableTableManager extends RootTableManager<
_$AppDatabase,
$BlacklistTableTable,
BlacklistTableData,
$$BlacklistTableTableFilterComposer,
$$BlacklistTableTableOrderingComposer,
$$BlacklistTableTableProcessedTableManager,
$$BlacklistTableTableInsertCompanionBuilder,
$$BlacklistTableTableUpdateCompanionBuilder> {
$$BlacklistTableTableTableManager(
_$AppDatabase db, $BlacklistTableTable table)
: super(TableManagerState(
db: db,
table: table,
filteringComposer:
$$BlacklistTableTableFilterComposer(ComposerState(db, table)),
orderingComposer:
$$BlacklistTableTableOrderingComposer(ComposerState(db, table)),
getChildManagerBuilder: (p) =>
$$BlacklistTableTableProcessedTableManager(p),
getUpdateCompanionBuilder: ({
Value<int> id = const Value.absent(),
Value<String> name = const Value.absent(),
Value<BlacklistedType> elementType = const Value.absent(),
Value<String> elementId = const Value.absent(),
}) =>
BlacklistTableCompanion(
id: id,
name: name,
elementType: elementType,
elementId: elementId,
),
getInsertCompanionBuilder: ({
Value<int> id = const Value.absent(),
required String name,
required BlacklistedType elementType,
required String elementId,
}) =>
BlacklistTableCompanion.insert(
id: id,
name: name,
elementType: elementType,
elementId: elementId,
),
));
}
class $$BlacklistTableTableProcessedTableManager extends ProcessedTableManager<
_$AppDatabase,
$BlacklistTableTable,
BlacklistTableData,
$$BlacklistTableTableFilterComposer,
$$BlacklistTableTableOrderingComposer,
$$BlacklistTableTableProcessedTableManager,
$$BlacklistTableTableInsertCompanionBuilder,
$$BlacklistTableTableUpdateCompanionBuilder> {
$$BlacklistTableTableProcessedTableManager(super.$state);
}
class $$BlacklistTableTableFilterComposer
extends FilterComposer<_$AppDatabase, $BlacklistTableTable> {
$$BlacklistTableTableFilterComposer(super.$state);
ColumnFilters<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<String> get name => $state.composableBuilder(
column: $state.table.name,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<BlacklistedType, BlacklistedType, String>
get elementType => $state.composableBuilder(
column: $state.table.elementType,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<String> get elementId => $state.composableBuilder(
column: $state.table.elementId,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
}
class $$BlacklistTableTableOrderingComposer
extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> {
$$BlacklistTableTableOrderingComposer(super.$state);
ColumnOrderings<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get name => $state.composableBuilder(
column: $state.table.name,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get elementType => $state.composableBuilder(
column: $state.table.elementType,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get elementId => $state.composableBuilder(
column: $state.table.elementId,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
}
class _$AppDatabaseManager { class _$AppDatabaseManager {
final _$AppDatabase _db; final _$AppDatabase _db;
_$AppDatabaseManager(this._db); _$AppDatabaseManager(this._db);
@ -2580,4 +2973,6 @@ class _$AppDatabaseManager {
$$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable); $$SourceMatchTableTableTableManager(_db, _db.sourceMatchTable);
$$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager get skipSegmentTable =>
$$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable);
$$BlacklistTableTableTableManager get blacklistTable =>
$$BlacklistTableTableTableManager(_db, _db.blacklistTable);
} }

View File

@ -0,0 +1,18 @@
part of '../database.dart';
enum BlacklistedType {
artist,
track;
}
@TableIndex(
name: "unique_blacklist",
unique: true,
columns: {#elementType, #elementId},
)
class BlacklistTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get elementType => textEnum<BlacklistedType>()();
TextColumn get elementId => text()();
}

View File

@ -27,8 +27,8 @@ class ArtistCard extends HookConsumerWidget {
); );
final isBlackListed = ref.watch( final isBlackListed = ref.watch(
blacklistProvider.select( blacklistProvider.select(
(blacklist) => blacklist.contains( (blacklist) => blacklist.asData?.value.any(
BlacklistedElement.artist(artist.id!, artist.name!), (element) => element.elementId == artist.id,
), ),
), ),
); );
@ -55,7 +55,7 @@ class ArtistCard extends HookConsumerWidget {
elevation: 3, elevation: 3,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: radius, borderRadius: radius,
side: isBlackListed side: isBlackListed == true
? const BorderSide( ? const BorderSide(
color: Colors.red, color: Colors.red,
width: 2, width: 2,

View File

@ -10,6 +10,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart'; import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
@ -39,10 +40,9 @@ class ArtistPageHeader extends HookConsumerWidget {
); );
final auth = ref.watch(authenticationProvider); final auth = ref.watch(authenticationProvider);
final blacklist = ref.watch(blacklistProvider); ref.watch(blacklistProvider);
final isBlackListed = blacklist.contains( final blacklistNotifier = ref.watch(blacklistProvider.notifier);
BlacklistedElement.artist(artistId, artist.name!), final isBlackListed = blacklistNotifier.containsArtist(artist);
);
final image = artist.images.asUrlString( final image = artist.images.asUrlString(
placeholder: ImagePlaceholder.artist, placeholder: ImagePlaceholder.artist,
@ -187,14 +187,16 @@ class ArtistPageHeader extends HookConsumerWidget {
), ),
onPressed: () async { onPressed: () async {
if (isBlackListed) { if (isBlackListed) {
ref.read(blacklistProvider.notifier).remove( await ref
BlacklistedElement.artist( .read(blacklistProvider.notifier)
artist.id!, artist.name!), .remove(artist.id!);
);
} else { } else {
ref.read(blacklistProvider.notifier).add( await ref.read(blacklistProvider.notifier).add(
BlacklistedElement.artist( BlacklistTableCompanion.insert(
artist.id!, artist.name!), name: artist.name!,
elementId: artist.id!,
elementType: BlacklistedType.artist,
),
); );
} }
}, },

View File

@ -24,19 +24,21 @@ class BlackListPage extends HookConsumerWidget {
final filteredBlacklist = useMemoized( final filteredBlacklist = useMemoized(
() { () {
if (searchText.value.isEmpty) { if (searchText.value.isEmpty) {
return blacklist; return blacklist.asData?.value ?? [];
} }
return blacklist return blacklist.asData?.value
.map( .map(
(e) => ( (e) => (
weightedRatio("${e.name} ${e.type.name}", searchText.value), weightedRatio(
e, "${e.name} ${e.elementType.name}", searchText.value),
), e,
) ),
.sorted((a, b) => b.$1.compareTo(a.$1)) )
.where((e) => e.$1 > 50) .sorted((a, b) => b.$1.compareTo(a.$1))
.map((e) => e.$2) .where((e) => e.$1 > 50)
.toList(); .map((e) => e.$2)
.toList() ??
[];
}, },
[blacklist, searchText.value], [blacklist, searchText.value],
); );
@ -70,14 +72,14 @@ class BlackListPage extends HookConsumerWidget {
final item = filteredBlacklist.elementAt(index); final item = filteredBlacklist.elementAt(index);
return ListTile( return ListTile(
leading: Text("${index + 1}."), leading: Text("${index + 1}."),
title: Text("${item.name} (${item.type.name})"), title: Text("${item.name} (${item.elementType.name})"),
subtitle: Text(item.id), subtitle: Text(item.elementId),
trailing: IconButton( trailing: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () { onPressed: () {
ref ref
.read(blacklistProvider.notifier) .read(blacklistProvider.notifier)
.remove(filteredBlacklist.elementAt(index)); .remove(filteredBlacklist.elementAt(index).elementId);
}, },
), ),
); );

View File

@ -2,69 +2,59 @@ import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/current_playlist.dart'; import 'package:spotube/models/current_playlist.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart';
enum BlacklistedType {
artist,
track;
static BlacklistedType fromName(String name) =>
BlacklistedType.values.firstWhere((e) => e.name == name);
}
class BlacklistedElement {
final String id;
final String name;
final BlacklistedType type;
BlacklistedElement.artist(this.id, this.name) : type = BlacklistedType.artist;
BlacklistedElement.track(this.id, this.name) : type = BlacklistedType.track;
BlacklistedElement.fromJson(Map<String, dynamic> json)
: id = json['id'],
name = json['name'],
type = BlacklistedType.fromName(json['type']);
Map<String, dynamic> toJson() => {'id': id, 'type': type.name, 'name': name};
class BlackListNotifier extends AsyncNotifier<List<BlacklistTableData>> {
@override @override
operator ==(other) => build() async {
other is BlacklistedElement && final database = ref.watch(databaseProvider);
other.id == id &&
other.type == type &&
other.name == name;
@override final subscription = database
int get hashCode => id.hashCode ^ type.hashCode ^ name.hashCode; .select(database.blacklistTable)
} .watch()
.listen((event) => state = AsyncData(event));
class BlackListNotifier ref.onDispose(() {
extends PersistedStateNotifier<Set<BlacklistedElement>> { subscription.cancel();
BlackListNotifier() : super({}, "blacklist"); });
void add(BlacklistedElement element) { return await database.select(database.blacklistTable).get();
state = state.union({element});
} }
void remove(BlacklistedElement element) { AppDatabase get _database => ref.read(databaseProvider);
state = state.difference({element});
Future<void> add(BlacklistTableCompanion element) async {
_database.into(_database.blacklistTable).insert(element);
}
Future<void> remove(String elementId) async {
await (_database.delete(_database.blacklistTable)
..where((tbl) => tbl.elementId.equals(elementId)))
.go();
} }
bool contains(TrackSimple track) { bool contains(TrackSimple track) {
final containsTrack = final containsTrack =
state.contains(BlacklistedElement.track(track.id!, track.name!)); state.asData?.value.any((element) => element.elementId == track.id) ??
false;
final containsTrackArtists = track.artists?.any( final containsTrackArtists = track.artists?.any(
(artist) => state.contains( (artist) =>
BlacklistedElement.artist(artist.id!, artist.name ?? "Spotify"), state.asData?.value.any((el) => el.elementId == artist.id) ??
), false,
) ?? ) ??
false; false;
return containsTrack || containsTrackArtists; return containsTrack || containsTrackArtists;
} }
bool containsArtist(ArtistSimple artist) {
return state.asData?.value
.any((element) => element.elementId == artist.id) ??
false;
}
/// Filters the non blacklisted tracks from the given [tracks] /// Filters the non blacklisted tracks from the given [tracks]
Iterable<TrackSimple> filter(Iterable<TrackSimple> tracks) { Iterable<TrackSimple> filter(Iterable<TrackSimple> tracks) {
return tracks.whereNot(contains).toList(); return tracks.whereNot(contains).toList();
@ -75,34 +65,12 @@ class BlackListNotifier
id: playlist.id, id: playlist.id,
name: playlist.name, name: playlist.name,
thumbnail: playlist.thumbnail, thumbnail: playlist.thumbnail,
tracks: playlist.tracks.where( tracks: playlist.tracks.where((track) => !contains(track)).toList(),
(track) {
return !state
.contains(BlacklistedElement.track(track.id!, track.name!)) &&
!(track.artists ?? []).any(
(artist) => state.contains(
BlacklistedElement.artist(artist.id!, artist.name!),
),
);
},
).toList(),
); );
} }
@override
Set<BlacklistedElement> fromJson(Map<String, dynamic> json) {
return json['blacklist']
.map<BlacklistedElement>((e) => BlacklistedElement.fromJson(e))
.toSet();
}
@override
Map<String, dynamic> toJson() {
return {'blacklist': state.map((e) => e.toJson()).toList()};
}
} }
final blacklistProvider = final blacklistProvider =
StateNotifierProvider<BlackListNotifier, Set<BlacklistedElement>>((ref) { AsyncNotifierProvider<BlackListNotifier, List<BlacklistTableData>>(
return BlackListNotifier(); () => BlackListNotifier(),
}); );