mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
chore: metadata
This commit is contained in:
parent
67713c60d4
commit
2d6fe886e2
1
drift_schemas/app_db/drift_schema_v7.json
Normal file
1
drift_schemas/app_db/drift_schema_v7.json
Normal file
File diff suppressed because one or more lines are too long
@ -102,6 +102,10 @@ class AppRouter extends RootStackRouter {
|
|||||||
path: "settings",
|
path: "settings",
|
||||||
page: SettingsRoute.page,
|
page: SettingsRoute.page,
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
path: "settings/metadata-provider",
|
||||||
|
page: SettingsMetadataProviderRoute.page,
|
||||||
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "settings/blacklist",
|
path: "settings/blacklist",
|
||||||
page: BlackListRoute.page,
|
page: BlackListRoute.page,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -135,4 +135,7 @@ abstract class SpotubeIcons {
|
|||||||
static const list = FeatherIcons.list;
|
static const list = FeatherIcons.list;
|
||||||
static const device = FeatherIcons.smartphone;
|
static const device = FeatherIcons.smartphone;
|
||||||
static const engine = FeatherIcons.server;
|
static const engine = FeatherIcons.server;
|
||||||
|
static const extensions = FeatherIcons.package;
|
||||||
|
static const message = FeatherIcons.send;
|
||||||
|
static const upload = FeatherIcons.uploadCloud;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
|||||||
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
import 'package:spotube/provider/audio_player/audio_player_streams.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/glance/glance.dart';
|
import 'package:spotube/provider/glance/glance.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/server/bonsoir.dart';
|
import 'package:spotube/provider/server/bonsoir.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
||||||
@ -145,6 +146,8 @@ class Spotube extends HookConsumerWidget {
|
|||||||
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
|
ref.listen(audioPlayerStreamListenersProvider, (_, __) {});
|
||||||
ref.listen(bonsoirProvider, (_, __) {});
|
ref.listen(bonsoirProvider, (_, __) {});
|
||||||
ref.listen(connectClientsProvider, (_, __) {});
|
ref.listen(connectClientsProvider, (_, __) {});
|
||||||
|
ref.listen(metadataPluginsProvider, (_, __) {});
|
||||||
|
ref.listen(metadataPluginApiProvider, (_, __) {});
|
||||||
ref.listen(serverProvider, (_, __) {});
|
ref.listen(serverProvider, (_, __) {});
|
||||||
ref.listen(trayManagerProvider, (_, __) {});
|
ref.listen(trayManagerProvider, (_, __) {});
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import 'dart:convert';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:drift/remote.dart';
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:media_kit/media_kit.dart' hide Track;
|
import 'package:media_kit/media_kit.dart' hide Track;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
@ -35,6 +36,7 @@ part 'tables/source_match.dart';
|
|||||||
part 'tables/audio_player_state.dart';
|
part 'tables/audio_player_state.dart';
|
||||||
part 'tables/history.dart';
|
part 'tables/history.dart';
|
||||||
part 'tables/lyrics.dart';
|
part 'tables/lyrics.dart';
|
||||||
|
part 'tables/metadata_plugins.dart';
|
||||||
|
|
||||||
part 'typeconverters/color.dart';
|
part 'typeconverters/color.dart';
|
||||||
part 'typeconverters/locale.dart';
|
part 'typeconverters/locale.dart';
|
||||||
@ -56,13 +58,14 @@ part 'typeconverters/subtitle.dart';
|
|||||||
PlaylistMediaTable,
|
PlaylistMediaTable,
|
||||||
HistoryTable,
|
HistoryTable,
|
||||||
LyricsTable,
|
LyricsTable,
|
||||||
|
MetadataPluginsTable,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase() : super(_openConnection());
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 6;
|
int get schemaVersion => 7;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@ -115,11 +118,21 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
from5To6: (m, schema) async {
|
from5To6: (m, schema) async {
|
||||||
// Add new column to preferences table
|
try {
|
||||||
await m.addColumn(
|
await m.addColumn(
|
||||||
schema.preferencesTable,
|
schema.preferencesTable,
|
||||||
schema.preferencesTable.connectPort,
|
schema.preferencesTable.connectPort,
|
||||||
);
|
);
|
||||||
|
} on DriftRemoteException catch (e) {
|
||||||
|
// If the column already exists, ignore the error
|
||||||
|
if (e.remoteCause !=
|
||||||
|
'duplicate column name: ${schema.preferencesTable.connectPort.name}') {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
from6To7: (m, schema) async {
|
||||||
|
await m.createTable(schema.metadataPluginsTable);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -4275,6 +4275,353 @@ class LyricsTableCompanion extends UpdateCompanion<LyricsTableData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class $MetadataPluginsTableTable extends MetadataPluginsTable
|
||||||
|
with TableInfo<$MetadataPluginsTableTable, MetadataPluginsTableData> {
|
||||||
|
@override
|
||||||
|
final GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$MetadataPluginsTableTable(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,
|
||||||
|
additionalChecks:
|
||||||
|
GeneratedColumn.checkTextLength(minTextLength: 1, maxTextLength: 50),
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _descriptionMeta =
|
||||||
|
const VerificationMeta('description');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _versionMeta =
|
||||||
|
const VerificationMeta('version');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> version = GeneratedColumn<String>(
|
||||||
|
'version', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _authorMeta = const VerificationMeta('author');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> author = GeneratedColumn<String>(
|
||||||
|
'author', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _selectedMeta =
|
||||||
|
const VerificationMeta('selected');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> selected = GeneratedColumn<bool>(
|
||||||
|
'selected', aliasedName, false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
GeneratedColumn.constraintIsAlways('CHECK ("selected" IN (0, 1))'),
|
||||||
|
defaultValue: const Constant(false));
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns =>
|
||||||
|
[id, name, description, version, author, selected];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'metadata_plugins_table';
|
||||||
|
@override
|
||||||
|
VerificationContext validateIntegrity(
|
||||||
|
Insertable<MetadataPluginsTableData> 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);
|
||||||
|
}
|
||||||
|
if (data.containsKey('description')) {
|
||||||
|
context.handle(
|
||||||
|
_descriptionMeta,
|
||||||
|
description.isAcceptableOrUnknown(
|
||||||
|
data['description']!, _descriptionMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_descriptionMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('version')) {
|
||||||
|
context.handle(_versionMeta,
|
||||||
|
version.isAcceptableOrUnknown(data['version']!, _versionMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_versionMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('author')) {
|
||||||
|
context.handle(_authorMeta,
|
||||||
|
author.isAcceptableOrUnknown(data['author']!, _authorMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_authorMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('selected')) {
|
||||||
|
context.handle(_selectedMeta,
|
||||||
|
selected.isAcceptableOrUnknown(data['selected']!, _selectedMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
MetadataPluginsTableData map(Map<String, dynamic> data,
|
||||||
|
{String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return MetadataPluginsTableData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||||
|
name: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||||
|
description: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}description'])!,
|
||||||
|
version: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}version'])!,
|
||||||
|
author: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.string, data['${effectivePrefix}author'])!,
|
||||||
|
selected: attachedDatabase.typeMapping
|
||||||
|
.read(DriftSqlType.bool, data['${effectivePrefix}selected'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$MetadataPluginsTableTable createAlias(String alias) {
|
||||||
|
return $MetadataPluginsTableTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetadataPluginsTableData extends DataClass
|
||||||
|
implements Insertable<MetadataPluginsTableData> {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final String version;
|
||||||
|
final String author;
|
||||||
|
final bool selected;
|
||||||
|
const MetadataPluginsTableData(
|
||||||
|
{required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.version,
|
||||||
|
required this.author,
|
||||||
|
required this.selected});
|
||||||
|
@override
|
||||||
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, Expression>{};
|
||||||
|
map['id'] = Variable<int>(id);
|
||||||
|
map['name'] = Variable<String>(name);
|
||||||
|
map['description'] = Variable<String>(description);
|
||||||
|
map['version'] = Variable<String>(version);
|
||||||
|
map['author'] = Variable<String>(author);
|
||||||
|
map['selected'] = Variable<bool>(selected);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
MetadataPluginsTableCompanion toCompanion(bool nullToAbsent) {
|
||||||
|
return MetadataPluginsTableCompanion(
|
||||||
|
id: Value(id),
|
||||||
|
name: Value(name),
|
||||||
|
description: Value(description),
|
||||||
|
version: Value(version),
|
||||||
|
author: Value(author),
|
||||||
|
selected: Value(selected),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MetadataPluginsTableData.fromJson(Map<String, dynamic> json,
|
||||||
|
{ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return MetadataPluginsTableData(
|
||||||
|
id: serializer.fromJson<int>(json['id']),
|
||||||
|
name: serializer.fromJson<String>(json['name']),
|
||||||
|
description: serializer.fromJson<String>(json['description']),
|
||||||
|
version: serializer.fromJson<String>(json['version']),
|
||||||
|
author: serializer.fromJson<String>(json['author']),
|
||||||
|
selected: serializer.fromJson<bool>(json['selected']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||||
|
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<int>(id),
|
||||||
|
'name': serializer.toJson<String>(name),
|
||||||
|
'description': serializer.toJson<String>(description),
|
||||||
|
'version': serializer.toJson<String>(version),
|
||||||
|
'author': serializer.toJson<String>(author),
|
||||||
|
'selected': serializer.toJson<bool>(selected),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
MetadataPluginsTableData copyWith(
|
||||||
|
{int? id,
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? version,
|
||||||
|
String? author,
|
||||||
|
bool? selected}) =>
|
||||||
|
MetadataPluginsTableData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
author: author ?? this.author,
|
||||||
|
selected: selected ?? this.selected,
|
||||||
|
);
|
||||||
|
MetadataPluginsTableData copyWithCompanion(
|
||||||
|
MetadataPluginsTableCompanion data) {
|
||||||
|
return MetadataPluginsTableData(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
name: data.name.present ? data.name.value : this.name,
|
||||||
|
description:
|
||||||
|
data.description.present ? data.description.value : this.description,
|
||||||
|
version: data.version.present ? data.version.value : this.version,
|
||||||
|
author: data.author.present ? data.author.value : this.author,
|
||||||
|
selected: data.selected.present ? data.selected.value : this.selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MetadataPluginsTableData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('description: $description, ')
|
||||||
|
..write('version: $version, ')
|
||||||
|
..write('author: $author, ')
|
||||||
|
..write('selected: $selected')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(id, name, description, version, author, selected);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is MetadataPluginsTableData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.name == this.name &&
|
||||||
|
other.description == this.description &&
|
||||||
|
other.version == this.version &&
|
||||||
|
other.author == this.author &&
|
||||||
|
other.selected == this.selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetadataPluginsTableCompanion
|
||||||
|
extends UpdateCompanion<MetadataPluginsTableData> {
|
||||||
|
final Value<int> id;
|
||||||
|
final Value<String> name;
|
||||||
|
final Value<String> description;
|
||||||
|
final Value<String> version;
|
||||||
|
final Value<String> author;
|
||||||
|
final Value<bool> selected;
|
||||||
|
const MetadataPluginsTableCompanion({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
this.name = const Value.absent(),
|
||||||
|
this.description = const Value.absent(),
|
||||||
|
this.version = const Value.absent(),
|
||||||
|
this.author = const Value.absent(),
|
||||||
|
this.selected = const Value.absent(),
|
||||||
|
});
|
||||||
|
MetadataPluginsTableCompanion.insert({
|
||||||
|
this.id = const Value.absent(),
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required String version,
|
||||||
|
required String author,
|
||||||
|
this.selected = const Value.absent(),
|
||||||
|
}) : name = Value(name),
|
||||||
|
description = Value(description),
|
||||||
|
version = Value(version),
|
||||||
|
author = Value(author);
|
||||||
|
static Insertable<MetadataPluginsTableData> custom({
|
||||||
|
Expression<int>? id,
|
||||||
|
Expression<String>? name,
|
||||||
|
Expression<String>? description,
|
||||||
|
Expression<String>? version,
|
||||||
|
Expression<String>? author,
|
||||||
|
Expression<bool>? selected,
|
||||||
|
}) {
|
||||||
|
return RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (name != null) 'name': name,
|
||||||
|
if (description != null) 'description': description,
|
||||||
|
if (version != null) 'version': version,
|
||||||
|
if (author != null) 'author': author,
|
||||||
|
if (selected != null) 'selected': selected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
MetadataPluginsTableCompanion copyWith(
|
||||||
|
{Value<int>? id,
|
||||||
|
Value<String>? name,
|
||||||
|
Value<String>? description,
|
||||||
|
Value<String>? version,
|
||||||
|
Value<String>? author,
|
||||||
|
Value<bool>? selected}) {
|
||||||
|
return MetadataPluginsTableCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
description: description ?? this.description,
|
||||||
|
version: version ?? this.version,
|
||||||
|
author: author ?? this.author,
|
||||||
|
selected: selected ?? this.selected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (description.present) {
|
||||||
|
map['description'] = Variable<String>(description.value);
|
||||||
|
}
|
||||||
|
if (version.present) {
|
||||||
|
map['version'] = Variable<String>(version.value);
|
||||||
|
}
|
||||||
|
if (author.present) {
|
||||||
|
map['author'] = Variable<String>(author.value);
|
||||||
|
}
|
||||||
|
if (selected.present) {
|
||||||
|
map['selected'] = Variable<bool>(selected.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MetadataPluginsTableCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('name: $name, ')
|
||||||
|
..write('description: $description, ')
|
||||||
|
..write('version: $version, ')
|
||||||
|
..write('author: $author, ')
|
||||||
|
..write('selected: $selected')
|
||||||
|
..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);
|
||||||
@ -4295,6 +4642,8 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
$PlaylistMediaTableTable(this);
|
$PlaylistMediaTableTable(this);
|
||||||
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
||||||
late final $LyricsTableTable lyricsTable = $LyricsTableTable(this);
|
late final $LyricsTableTable lyricsTable = $LyricsTableTable(this);
|
||||||
|
late final $MetadataPluginsTableTable metadataPluginsTable =
|
||||||
|
$MetadataPluginsTableTable(this);
|
||||||
late final Index uniqueBlacklist = Index('unique_blacklist',
|
late final Index uniqueBlacklist = Index('unique_blacklist',
|
||||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||||
late final Index uniqTrackMatch = Index('uniq_track_match',
|
late final Index uniqTrackMatch = Index('uniq_track_match',
|
||||||
@ -4315,6 +4664,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
playlistMediaTable,
|
playlistMediaTable,
|
||||||
historyTable,
|
historyTable,
|
||||||
lyricsTable,
|
lyricsTable,
|
||||||
|
metadataPluginsTable,
|
||||||
uniqueBlacklist,
|
uniqueBlacklist,
|
||||||
uniqTrackMatch
|
uniqTrackMatch
|
||||||
];
|
];
|
||||||
@ -6909,6 +7259,194 @@ typedef $$LyricsTableTableProcessedTableManager = ProcessedTableManager<
|
|||||||
),
|
),
|
||||||
LyricsTableData,
|
LyricsTableData,
|
||||||
PrefetchHooks Function()>;
|
PrefetchHooks Function()>;
|
||||||
|
typedef $$MetadataPluginsTableTableCreateCompanionBuilder
|
||||||
|
= MetadataPluginsTableCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required String version,
|
||||||
|
required String author,
|
||||||
|
Value<bool> selected,
|
||||||
|
});
|
||||||
|
typedef $$MetadataPluginsTableTableUpdateCompanionBuilder
|
||||||
|
= MetadataPluginsTableCompanion Function({
|
||||||
|
Value<int> id,
|
||||||
|
Value<String> name,
|
||||||
|
Value<String> description,
|
||||||
|
Value<String> version,
|
||||||
|
Value<String> author,
|
||||||
|
Value<bool> selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
class $$MetadataPluginsTableTableFilterComposer
|
||||||
|
extends Composer<_$AppDatabase, $MetadataPluginsTableTable> {
|
||||||
|
$$MetadataPluginsTableTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnFilters<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get name => $composableBuilder(
|
||||||
|
column: $table.name, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get description => $composableBuilder(
|
||||||
|
column: $table.description, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get version => $composableBuilder(
|
||||||
|
column: $table.version, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<String> get author => $composableBuilder(
|
||||||
|
column: $table.author, builder: (column) => ColumnFilters(column));
|
||||||
|
|
||||||
|
ColumnFilters<bool> get selected => $composableBuilder(
|
||||||
|
column: $table.selected, builder: (column) => ColumnFilters(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MetadataPluginsTableTableOrderingComposer
|
||||||
|
extends Composer<_$AppDatabase, $MetadataPluginsTableTable> {
|
||||||
|
$$MetadataPluginsTableTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
ColumnOrderings<int> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get name => $composableBuilder(
|
||||||
|
column: $table.name, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get description => $composableBuilder(
|
||||||
|
column: $table.description, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get version => $composableBuilder(
|
||||||
|
column: $table.version, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get author => $composableBuilder(
|
||||||
|
column: $table.author, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get selected => $composableBuilder(
|
||||||
|
column: $table.selected, builder: (column) => ColumnOrderings(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MetadataPluginsTableTableAnnotationComposer
|
||||||
|
extends Composer<_$AppDatabase, $MetadataPluginsTableTable> {
|
||||||
|
$$MetadataPluginsTableTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
GeneratedColumn<int> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get name =>
|
||||||
|
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get description => $composableBuilder(
|
||||||
|
column: $table.description, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get version =>
|
||||||
|
$composableBuilder(column: $table.version, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get author =>
|
||||||
|
$composableBuilder(column: $table.author, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<bool> get selected =>
|
||||||
|
$composableBuilder(column: $table.selected, builder: (column) => column);
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MetadataPluginsTableTableTableManager extends RootTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$MetadataPluginsTableTable,
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
$$MetadataPluginsTableTableFilterComposer,
|
||||||
|
$$MetadataPluginsTableTableOrderingComposer,
|
||||||
|
$$MetadataPluginsTableTableAnnotationComposer,
|
||||||
|
$$MetadataPluginsTableTableCreateCompanionBuilder,
|
||||||
|
$$MetadataPluginsTableTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
BaseReferences<_$AppDatabase, $MetadataPluginsTableTable,
|
||||||
|
MetadataPluginsTableData>
|
||||||
|
),
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
PrefetchHooks Function()> {
|
||||||
|
$$MetadataPluginsTableTableTableManager(
|
||||||
|
_$AppDatabase db, $MetadataPluginsTableTable table)
|
||||||
|
: super(TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
$$MetadataPluginsTableTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
$$MetadataPluginsTableTableOrderingComposer(
|
||||||
|
$db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
$$MetadataPluginsTableTableAnnotationComposer(
|
||||||
|
$db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
Value<String> name = const Value.absent(),
|
||||||
|
Value<String> description = const Value.absent(),
|
||||||
|
Value<String> version = const Value.absent(),
|
||||||
|
Value<String> author = const Value.absent(),
|
||||||
|
Value<bool> selected = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
MetadataPluginsTableCompanion(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
version: version,
|
||||||
|
author: author,
|
||||||
|
selected: selected,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
Value<int> id = const Value.absent(),
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required String version,
|
||||||
|
required String author,
|
||||||
|
Value<bool> selected = const Value.absent(),
|
||||||
|
}) =>
|
||||||
|
MetadataPluginsTableCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
version: version,
|
||||||
|
author: author,
|
||||||
|
selected: selected,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: null,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$MetadataPluginsTableTableProcessedTableManager
|
||||||
|
= ProcessedTableManager<
|
||||||
|
_$AppDatabase,
|
||||||
|
$MetadataPluginsTableTable,
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
$$MetadataPluginsTableTableFilterComposer,
|
||||||
|
$$MetadataPluginsTableTableOrderingComposer,
|
||||||
|
$$MetadataPluginsTableTableAnnotationComposer,
|
||||||
|
$$MetadataPluginsTableTableCreateCompanionBuilder,
|
||||||
|
$$MetadataPluginsTableTableUpdateCompanionBuilder,
|
||||||
|
(
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
BaseReferences<_$AppDatabase, $MetadataPluginsTableTable,
|
||||||
|
MetadataPluginsTableData>
|
||||||
|
),
|
||||||
|
MetadataPluginsTableData,
|
||||||
|
PrefetchHooks Function()>;
|
||||||
|
|
||||||
class $AppDatabaseManager {
|
class $AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
@ -6935,4 +7473,6 @@ class $AppDatabaseManager {
|
|||||||
$$HistoryTableTableTableManager(_db, _db.historyTable);
|
$$HistoryTableTableTableManager(_db, _db.historyTable);
|
||||||
$$LyricsTableTableTableManager get lyricsTable =>
|
$$LyricsTableTableTableManager get lyricsTable =>
|
||||||
$$LyricsTableTableTableManager(_db, _db.lyricsTable);
|
$$LyricsTableTableTableManager(_db, _db.lyricsTable);
|
||||||
|
$$MetadataPluginsTableTableTableManager get metadataPluginsTable =>
|
||||||
|
$$MetadataPluginsTableTableTableManager(_db, _db.metadataPluginsTable);
|
||||||
}
|
}
|
||||||
|
@ -1692,12 +1692,285 @@ class Shape13 extends i0.VersionedTable {
|
|||||||
i1.GeneratedColumn<int> _column_56(String aliasedName) =>
|
i1.GeneratedColumn<int> _column_56(String aliasedName) =>
|
||||||
i1.GeneratedColumn<int>('connect_port', aliasedName, false,
|
i1.GeneratedColumn<int>('connect_port', aliasedName, false,
|
||||||
type: i1.DriftSqlType.int, defaultValue: const Constant(-1));
|
type: i1.DriftSqlType.int, defaultValue: const Constant(-1));
|
||||||
|
|
||||||
|
final class Schema7 extends i0.VersionedSchema {
|
||||||
|
Schema7({required super.database}) : super(version: 7);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
authenticationTable,
|
||||||
|
blacklistTable,
|
||||||
|
preferencesTable,
|
||||||
|
scrobblerTable,
|
||||||
|
skipSegmentTable,
|
||||||
|
sourceMatchTable,
|
||||||
|
audioPlayerStateTable,
|
||||||
|
playlistTable,
|
||||||
|
playlistMediaTable,
|
||||||
|
historyTable,
|
||||||
|
lyricsTable,
|
||||||
|
metadataPluginsTable,
|
||||||
|
uniqueBlacklist,
|
||||||
|
uniqTrackMatch,
|
||||||
|
];
|
||||||
|
late final Shape0 authenticationTable = Shape0(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'authentication_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_2,
|
||||||
|
_column_3,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape1 blacklistTable = Shape1(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'blacklist_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_4,
|
||||||
|
_column_5,
|
||||||
|
_column_6,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape13 preferencesTable = Shape13(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'preferences_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_7,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_55,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_22,
|
||||||
|
_column_23,
|
||||||
|
_column_24,
|
||||||
|
_column_25,
|
||||||
|
_column_26,
|
||||||
|
_column_54,
|
||||||
|
_column_27,
|
||||||
|
_column_28,
|
||||||
|
_column_29,
|
||||||
|
_column_30,
|
||||||
|
_column_31,
|
||||||
|
_column_56,
|
||||||
|
_column_53,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape3 scrobblerTable = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'scrobbler_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_32,
|
||||||
|
_column_33,
|
||||||
|
_column_34,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape4 skipSegmentTable = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'skip_segment_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_35,
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_32,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape5 sourceMatchTable = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'source_match_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_32,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape6 audioPlayerStateTable = Shape6(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'audio_player_state_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape7 playlistTable = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'playlist_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape8 playlistMediaTable = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'playlist_media_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape9 historyTable = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'history_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_32,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape10 lyricsTable = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'lyrics_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_37,
|
||||||
|
_column_52,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
late final Shape14 metadataPluginsTable = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'metadata_plugins_table',
|
||||||
|
withoutRowId: false,
|
||||||
|
isStrict: false,
|
||||||
|
tableConstraints: [],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
_column_60,
|
||||||
|
_column_61,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null);
|
||||||
|
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
|
||||||
|
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||||
|
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
|
||||||
|
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape14 extends i0.VersionedTable {
|
||||||
|
Shape14({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<int> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get description =>
|
||||||
|
columnsByName['description']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get version =>
|
||||||
|
columnsByName['version']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get author =>
|
||||||
|
columnsByName['author']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get selected =>
|
||||||
|
columnsByName['selected']! as i1.GeneratedColumn<bool>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<String> _column_57(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('name', aliasedName, false,
|
||||||
|
additionalChecks: i1.GeneratedColumn.checkTextLength(
|
||||||
|
minTextLength: 1, maxTextLength: 50),
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_58(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('description', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_59(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('version', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<String> _column_60(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<String>('author', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.string);
|
||||||
|
i1.GeneratedColumn<bool> _column_61(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<bool>('selected', aliasedName, false,
|
||||||
|
type: i1.DriftSqlType.bool,
|
||||||
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("selected" IN (0, 1))'),
|
||||||
|
defaultValue: const Constant(false));
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@ -1726,6 +1999,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from5To6(migrator, schema);
|
await from5To6(migrator, schema);
|
||||||
return 6;
|
return 6;
|
||||||
|
case 6:
|
||||||
|
final schema = Schema7(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from6To7(migrator, schema);
|
||||||
|
return 7;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@ -1738,6 +2016,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
required Future<void> Function(i1.Migrator m, Schema5 schema) from4To5,
|
||||||
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
@ -1746,4 +2025,5 @@ i1.OnUpgrade stepByStep({
|
|||||||
from3To4: from3To4,
|
from3To4: from3To4,
|
||||||
from4To5: from4To5,
|
from4To5: from4To5,
|
||||||
from5To6: from5To6,
|
from5To6: from5To6,
|
||||||
|
from6To7: from6To7,
|
||||||
));
|
));
|
||||||
|
10
lib/models/database/tables/metadata_plugins.dart
Normal file
10
lib/models/database/tables/metadata_plugins.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
part of '../database.dart';
|
||||||
|
|
||||||
|
class MetadataPluginsTable extends Table {
|
||||||
|
IntColumn get id => integer().autoIncrement()();
|
||||||
|
TextColumn get name => text().withLength(min: 1, max: 50)();
|
||||||
|
TextColumn get description => text()();
|
||||||
|
TextColumn get version => text()();
|
||||||
|
TextColumn get author => text()();
|
||||||
|
BoolColumn get selected => boolean().withDefault(const Constant(false))();
|
||||||
|
}
|
182
lib/pages/settings/metadata_plugins.dart
Normal file
182
lib/pages/settings/metadata_plugins.dart
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||||
|
const SettingsMetadataProviderPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
|
final plugins = ref.watch(metadataPluginsProvider);
|
||||||
|
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||||
|
final metadataApi = ref.watch(metadataPluginApiProvider);
|
||||||
|
final isAuthenticated = ref.watch(metadataAuthenticatedProvider);
|
||||||
|
|
||||||
|
final artists = ref.watch(metadataUserArtistsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
headers: const [
|
||||||
|
TitleBar(
|
||||||
|
title: Text("Metadata provider plugin"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilder(
|
||||||
|
key: formKey,
|
||||||
|
child: TextFormBuilderField(
|
||||||
|
name: "plugin_url",
|
||||||
|
validator: FormBuilderValidators.url(
|
||||||
|
protocols: ["http", "https"]),
|
||||||
|
placeholder: const Text(
|
||||||
|
"Add GitHub/Codeberg URL to plugin repository "
|
||||||
|
"or direct link to .smplug file",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
tooltip: const TooltipContainer(
|
||||||
|
child: Text("Download and install plugin from url"),
|
||||||
|
).call,
|
||||||
|
child: IconButton.secondary(
|
||||||
|
icon: const Icon(SpotubeIcons.download),
|
||||||
|
onPressed: () async {
|
||||||
|
if (formKey.currentState?.saveAndValidate() ?? false) {
|
||||||
|
final url = formKey.currentState?.fields["plugin_url"]
|
||||||
|
?.value as String;
|
||||||
|
|
||||||
|
if (url.isNotEmpty) {
|
||||||
|
final pluginConfig = await pluginsNotifier
|
||||||
|
.downloadAndCachePlugin(url);
|
||||||
|
|
||||||
|
await pluginsNotifier.addPlugin(pluginConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
tooltip: const TooltipContainer(
|
||||||
|
child: Text("Upload plugin from file"),
|
||||||
|
).call,
|
||||||
|
child: IconButton.primary(
|
||||||
|
icon: const Icon(SpotubeIcons.upload),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ["smplug"],
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final file = result.files.first;
|
||||||
|
|
||||||
|
if (file.bytes == null) return;
|
||||||
|
|
||||||
|
final pluginConfig = await pluginsNotifier
|
||||||
|
.extractPluginArchive(file.bytes!);
|
||||||
|
await pluginsNotifier.addPlugin(pluginConfig);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverGap(20),
|
||||||
|
SliverList.separated(
|
||||||
|
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||||
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final plugin = plugins.asData!.value.plugins[index];
|
||||||
|
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
||||||
|
final requiresAuth = isDefault &&
|
||||||
|
metadataApi.hasValue &&
|
||||||
|
metadataApi.asData?.value?.signatureFlags.requiresAuth ==
|
||||||
|
true;
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
spacing: 8,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Basic(
|
||||||
|
title: Text(plugin.name),
|
||||||
|
subtitle: Text(plugin.description),
|
||||||
|
trailing: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Button.primary(
|
||||||
|
enabled: !isDefault,
|
||||||
|
onPressed: () async {
|
||||||
|
await pluginsNotifier.setDefaultPlugin(plugin);
|
||||||
|
},
|
||||||
|
child: isDefault
|
||||||
|
? const Text("Default")
|
||||||
|
: const Text("Make default"),
|
||||||
|
),
|
||||||
|
IconButton.destructive(
|
||||||
|
onPressed: () async {
|
||||||
|
await pluginsNotifier.removePlugin(plugin);
|
||||||
|
},
|
||||||
|
icon: const Icon(SpotubeIcons.trash),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (requiresAuth)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text("Plugin requires authentication"),
|
||||||
|
const Spacer(),
|
||||||
|
if (isAuthenticated.asData?.value != true)
|
||||||
|
Button.primary(
|
||||||
|
onPressed: () async {
|
||||||
|
await metadataApi.asData?.value
|
||||||
|
?.authenticate();
|
||||||
|
},
|
||||||
|
leading: const Icon(SpotubeIcons.login),
|
||||||
|
child: const Text("Login"),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Button.destructive(
|
||||||
|
onPressed: () async {
|
||||||
|
await metadataApi.asData?.value?.logout();
|
||||||
|
},
|
||||||
|
leading: const Icon(SpotubeIcons.logout),
|
||||||
|
child: const Text("Logout"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:auto_size_text/auto_size_text.dart';
|
|
||||||
import 'package:flutter/material.dart' show ListTile;
|
import 'package:flutter/material.dart' show ListTile;
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -7,103 +6,29 @@ import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|||||||
import 'package:spotube/collections/routes.gr.dart';
|
import 'package:spotube/collections/routes.gr.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/image/universal_image.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/pages/mobile_login/hooks/login_callback.dart';
|
|
||||||
import 'package:spotube/provider/authentication/authentication.dart';
|
|
||||||
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
import 'package:spotube/provider/scrobbler/scrobbler.dart';
|
||||||
import 'package:spotube/provider/spotify/spotify.dart';
|
|
||||||
|
|
||||||
class SettingsAccountSection extends HookConsumerWidget {
|
class SettingsAccountSection extends HookConsumerWidget {
|
||||||
const SettingsAccountSection({super.key});
|
const SettingsAccountSection({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(context, ref) {
|
Widget build(context, ref) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
final auth = ref.watch(authenticationProvider);
|
|
||||||
final scrobbler = ref.watch(scrobblerProvider);
|
final scrobbler = ref.watch(scrobblerProvider);
|
||||||
final me = ref.watch(meProvider);
|
|
||||||
final meData = me.asData?.value;
|
|
||||||
|
|
||||||
final onLogin = useLoginCallback(ref);
|
|
||||||
|
|
||||||
return SectionCardWithHeading(
|
return SectionCardWithHeading(
|
||||||
heading: context.l10n.account,
|
heading: context.l10n.account,
|
||||||
children: [
|
children: [
|
||||||
if (auth.asData?.value != null)
|
ListTile(
|
||||||
ListTile(
|
leading: const Icon(SpotubeIcons.extensions),
|
||||||
leading: const Icon(SpotubeIcons.user),
|
title: const Text("Metadata provider plugins"),
|
||||||
title: Text(context.l10n.user_profile),
|
subtitle: const Text(
|
||||||
trailing: Padding(
|
"Configure your own playlist/album/artist/feed metadata provider"),
|
||||||
padding: const EdgeInsets.all(8.0),
|
onTap: () {
|
||||||
child: Avatar(
|
context.pushRoute(const SettingsMetadataProviderRoute());
|
||||||
initials: Avatar.getInitials(meData?.displayName ?? "User"),
|
},
|
||||||
provider: UniversalImage.imageProvider(
|
trailing: const Icon(SpotubeIcons.angleRight),
|
||||||
(meData?.images).asUrlString(
|
),
|
||||||
placeholder: ImagePlaceholder.artist,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
context.navigateTo(const ProfileRoute());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (auth.asData?.value == null)
|
|
||||||
LayoutBuilder(builder: (context, constrains) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
SpotubeIcons.spotify,
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
title: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: AutoSizeText(
|
|
||||||
context.l10n.login_with_spotify,
|
|
||||||
maxLines: 1,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onTap: constrains.mdAndUp ? null : onLogin,
|
|
||||||
trailing: constrains.smAndDown
|
|
||||||
? null
|
|
||||||
: Button.primary(
|
|
||||||
onPressed: onLogin,
|
|
||||||
child: Text(
|
|
||||||
context.l10n.connect_with_spotify.toUpperCase(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
else
|
|
||||||
Builder(builder: (context) {
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(SpotubeIcons.spotify),
|
|
||||||
title: SizedBox(
|
|
||||||
height: 50,
|
|
||||||
width: 180,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: AutoSizeText(
|
|
||||||
context.l10n.logout_of_this_account,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Button.destructive(
|
|
||||||
onPressed: () async {
|
|
||||||
ref.read(authenticationProvider.notifier).logout();
|
|
||||||
context.maybePop();
|
|
||||||
},
|
|
||||||
child: Text(context.l10n.logout),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
if (scrobbler.asData?.value == null)
|
if (scrobbler.asData?.value == null)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(SpotubeIcons.lastFm),
|
leading: const Icon(SpotubeIcons.lastFm),
|
||||||
|
49
lib/provider/metadata_plugin/auth.dart
Normal file
49
lib/provider/metadata_plugin/auth.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
|
||||||
|
class MetadataAuthenticationNotifier extends AsyncNotifier<bool> {
|
||||||
|
MetadataAuthenticationNotifier();
|
||||||
|
@override
|
||||||
|
build() async {
|
||||||
|
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
|
||||||
|
|
||||||
|
if (metadataApi?.signatureFlags.requiresAuth != true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final subscription = metadataApi?.authenticatedStream.listen((event) {
|
||||||
|
state = AsyncValue.data(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription?.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
return await metadataApi?.isAuthenticated() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login() async {
|
||||||
|
final metadataApi = await ref.read(metadataPluginApiProvider.future);
|
||||||
|
|
||||||
|
if (metadataApi == null || !metadataApi.signatureFlags.requiresAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await metadataApi.authenticate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
final metadataApi = await ref.read(metadataPluginApiProvider.future);
|
||||||
|
|
||||||
|
if (metadataApi == null || !metadataApi.signatureFlags.requiresAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await metadataApi.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadataAuthenticatedProvider =
|
||||||
|
AsyncNotifierProvider<MetadataAuthenticationNotifier, bool>(
|
||||||
|
() => MetadataAuthenticationNotifier(),
|
||||||
|
);
|
365
lib/provider/metadata_plugin/metadata_plugin_provider.dart
Normal file
365
lib/provider/metadata_plugin/metadata_plugin_provider.dart
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/auth.dart';
|
||||||
|
import 'package:spotube/services/dio/dio.dart';
|
||||||
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:archive/archive.dart';
|
||||||
|
|
||||||
|
final allowedDomainsRegex = RegExp(
|
||||||
|
r"^(https?:\/\/)?(www\.)?(github\.com|codeberg\.org)\/.+",
|
||||||
|
);
|
||||||
|
|
||||||
|
class MetadataPluginState {
|
||||||
|
final List<PluginConfiguration> plugins;
|
||||||
|
final int defaultPlugin;
|
||||||
|
|
||||||
|
const MetadataPluginState({
|
||||||
|
this.plugins = const [],
|
||||||
|
this.defaultPlugin = -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
PluginConfiguration? get defaultPluginConfig {
|
||||||
|
if (defaultPlugin < 0 || defaultPlugin >= plugins.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return plugins[defaultPlugin];
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MetadataPluginState.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MetadataPluginState(
|
||||||
|
plugins: (json["plugins"] as List<dynamic>)
|
||||||
|
.map((e) => PluginConfiguration.fromJson(e))
|
||||||
|
.toList(),
|
||||||
|
defaultPlugin: json["default_plugin"] ?? -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
"plugins": plugins.map((e) => e.toJson()).toList(),
|
||||||
|
"default_plugin": defaultPlugin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
MetadataPluginState copyWith({
|
||||||
|
List<PluginConfiguration>? plugins,
|
||||||
|
int? defaultPlugin,
|
||||||
|
}) {
|
||||||
|
return MetadataPluginState(
|
||||||
|
plugins: plugins ?? this.plugins,
|
||||||
|
defaultPlugin: defaultPlugin ?? this.defaultPlugin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
|
||||||
|
AppDatabase get database => ref.read(databaseProvider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
build() async {
|
||||||
|
final database = ref.watch(databaseProvider);
|
||||||
|
|
||||||
|
final subscription = database.metadataPluginsTable.select().watch().listen(
|
||||||
|
(event) async {
|
||||||
|
state = AsyncValue.data(await toStatePlugins(event));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
final plugins = await database.metadataPluginsTable.select().get();
|
||||||
|
|
||||||
|
return await toStatePlugins(plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<MetadataPluginState> toStatePlugins(
|
||||||
|
List<MetadataPluginsTableData> plugins) async {
|
||||||
|
int defaultPlugin = -1;
|
||||||
|
final pluginConfigs = plugins.mapIndexed(
|
||||||
|
(index, plugin) {
|
||||||
|
if (plugin.selected) {
|
||||||
|
defaultPlugin = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PluginConfiguration(
|
||||||
|
type: PluginType.metadata,
|
||||||
|
name: plugin.name,
|
||||||
|
author: plugin.author,
|
||||||
|
description: plugin.description,
|
||||||
|
version: plugin.version,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList();
|
||||||
|
return MetadataPluginState(
|
||||||
|
plugins: pluginConfigs,
|
||||||
|
defaultPlugin: defaultPlugin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri _getGithubReleasesUrl(String repoUrl) {
|
||||||
|
final parsedUri = Uri.parse(repoUrl);
|
||||||
|
final uri = parsedUri.replace(
|
||||||
|
host: "api.github.com",
|
||||||
|
path: "/repos/${parsedUri.path}/releases",
|
||||||
|
queryParameters: {
|
||||||
|
"per_page": "1",
|
||||||
|
"page": "1",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri _getCodebergeReleasesUrl(String repoUrl) {
|
||||||
|
final parsedUri = Uri.parse(repoUrl);
|
||||||
|
final uri = parsedUri.replace(
|
||||||
|
path: "/api/v1/repos/${parsedUri.path}/releases",
|
||||||
|
queryParameters: {
|
||||||
|
"limit": "1",
|
||||||
|
"page": "1",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getPluginDownloadUrl(Uri uri) async {
|
||||||
|
final res = await globalDio.getUri(
|
||||||
|
uri,
|
||||||
|
options: Options(responseType: ResponseType.json),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
throw Exception("Failed to get releases");
|
||||||
|
}
|
||||||
|
final releases = res.data as List;
|
||||||
|
if (releases.isEmpty) {
|
||||||
|
throw Exception("No releases found");
|
||||||
|
}
|
||||||
|
final latestRelease = releases.first;
|
||||||
|
final downloadUrl = (latestRelease["assets"] as List).firstWhere(
|
||||||
|
(asset) => (asset["name"] as String).endsWith(".smplug"),
|
||||||
|
)["browser_download_url"];
|
||||||
|
if (downloadUrl == null) {
|
||||||
|
throw Exception("No download URL found");
|
||||||
|
}
|
||||||
|
return downloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Directory> _getPluginDir() async => Directory(
|
||||||
|
join(
|
||||||
|
(await getApplicationCacheDirectory()).path,
|
||||||
|
"metadata-plugins",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<PluginConfiguration> extractPluginArchive(List<int> bytes) async {
|
||||||
|
final archive = ZipDecoder().decodeBytes(bytes);
|
||||||
|
final pluginJson = archive
|
||||||
|
.firstWhereOrNull((file) => file.isFile && file.name == "plugin.json");
|
||||||
|
|
||||||
|
if (pluginJson == null) {
|
||||||
|
throw Exception("No plugin.json found");
|
||||||
|
}
|
||||||
|
final pluginConfig = PluginConfiguration.fromJson(
|
||||||
|
jsonDecode(
|
||||||
|
utf8.decode(pluginJson.content as List<int>),
|
||||||
|
) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
|
||||||
|
final pluginDir = await _getPluginDir();
|
||||||
|
await pluginDir.create(recursive: true);
|
||||||
|
|
||||||
|
final pluginExtractionDirPath = join(
|
||||||
|
pluginDir.path,
|
||||||
|
ServiceUtils.sanitizeFilename(pluginConfig.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (final file in archive) {
|
||||||
|
if (file.isFile) {
|
||||||
|
final filename = file.name;
|
||||||
|
final data = file.content as List<int>;
|
||||||
|
final extractedFile = File(join(
|
||||||
|
pluginExtractionDirPath,
|
||||||
|
filename,
|
||||||
|
));
|
||||||
|
await extractedFile.create(recursive: true);
|
||||||
|
await extractedFile.writeAsBytes(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads, extracts & caches the plugin from the given URL and returns the plugin config.
|
||||||
|
/// If only a text/html URL is provided, it will try to get the latest release from
|
||||||
|
/// the URL for supported websites (github.com, codeberg.org).
|
||||||
|
Future<PluginConfiguration> downloadAndCachePlugin(String url) async {
|
||||||
|
final res = await globalDio.head(url);
|
||||||
|
final isSupportedWebsite =
|
||||||
|
(res.headers["Content-Type"] as String?)?.startsWith("text/html") ==
|
||||||
|
true &&
|
||||||
|
allowedDomainsRegex.hasMatch(url);
|
||||||
|
String pluginDownloadUrl = url;
|
||||||
|
if (isSupportedWebsite) {
|
||||||
|
if (url.contains("github.com")) {
|
||||||
|
final uri = _getGithubReleasesUrl(url);
|
||||||
|
pluginDownloadUrl = await _getPluginDownloadUrl(uri);
|
||||||
|
} else if (url.contains("codeberg.org")) {
|
||||||
|
final uri = _getCodebergeReleasesUrl(url);
|
||||||
|
pluginDownloadUrl = await _getPluginDownloadUrl(uri);
|
||||||
|
} else {
|
||||||
|
throw Exception("Unsupported website");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's download, extract and cache the plugin
|
||||||
|
final pluginDir = await _getPluginDir();
|
||||||
|
await pluginDir.create(recursive: true);
|
||||||
|
|
||||||
|
final tempPluginName = "${const Uuid().v4()}.smplug";
|
||||||
|
final pluginFile = File(join(pluginDir.path, tempPluginName));
|
||||||
|
|
||||||
|
final pluginRes = await globalDio.download(
|
||||||
|
pluginDownloadUrl,
|
||||||
|
pluginFile.path,
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
followRedirects: true,
|
||||||
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((pluginRes.statusCode ?? 500) > 299) {
|
||||||
|
throw Exception("Failed to download plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await extractPluginArchive(await pluginFile.readAsBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPlugin(PluginConfiguration plugin) async {
|
||||||
|
final pluginRes = await (database.metadataPluginsTable.select()
|
||||||
|
..where(
|
||||||
|
(tbl) => tbl.name.equals(plugin.name),
|
||||||
|
)
|
||||||
|
..limit(1))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (pluginRes.isNotEmpty) {
|
||||||
|
throw Exception("Plugin already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.metadataPluginsTable.insertOne(
|
||||||
|
MetadataPluginsTableCompanion.insert(
|
||||||
|
name: plugin.name,
|
||||||
|
author: plugin.author,
|
||||||
|
description: plugin.description,
|
||||||
|
version: plugin.version,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removePlugin(PluginConfiguration plugin) async {
|
||||||
|
final pluginDir = await _getPluginDir();
|
||||||
|
final pluginExtractionDirPath = join(
|
||||||
|
pluginDir.path,
|
||||||
|
ServiceUtils.sanitizeFilename(plugin.name),
|
||||||
|
);
|
||||||
|
final pluginExtractionDir = Directory(pluginExtractionDirPath);
|
||||||
|
if (pluginExtractionDir.existsSync()) {
|
||||||
|
await pluginExtractionDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
await database.metadataPluginsTable
|
||||||
|
.deleteWhere((tbl) => tbl.name.equals(plugin.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setDefaultPlugin(PluginConfiguration plugin) async {
|
||||||
|
await (database.metadataPluginsTable.update()
|
||||||
|
..where((tbl) => tbl.name.equals(plugin.name)))
|
||||||
|
.write(
|
||||||
|
const MetadataPluginsTableCompanion(selected: Value(true)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> getPluginLibraryCode(PluginConfiguration plugin) async {
|
||||||
|
final pluginDir = await _getPluginDir();
|
||||||
|
final pluginExtractionDirPath = join(
|
||||||
|
pluginDir.path,
|
||||||
|
ServiceUtils.sanitizeFilename(plugin.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
final libraryFile = File(join(pluginExtractionDirPath, "dist", "index.js"));
|
||||||
|
|
||||||
|
if (!libraryFile.existsSync()) {
|
||||||
|
throw Exception("No dist/index.js found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await libraryFile.readAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadataPluginsProvider =
|
||||||
|
AsyncNotifierProvider<MetadataPluginNotifier, MetadataPluginState>(
|
||||||
|
MetadataPluginNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
final metadataPluginApiProvider = FutureProvider<MetadataApiSignature?>(
|
||||||
|
(ref) async {
|
||||||
|
final defaultPlugin = await ref.watch(
|
||||||
|
metadataPluginsProvider.selectAsync((data) => data.defaultPluginConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (defaultPlugin == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||||
|
final libraryCode =
|
||||||
|
await pluginsNotifier.getPluginLibraryCode(defaultPlugin);
|
||||||
|
|
||||||
|
return MetadataApiSignature.init(libraryCode, defaultPlugin);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final metadataProviderUserProvider = FutureProvider(
|
||||||
|
(ref) async {
|
||||||
|
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
|
||||||
|
ref.watch(metadataAuthenticatedProvider);
|
||||||
|
|
||||||
|
if (metadataApi == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return metadataApi.getMe();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final metadataUserArtistsProvider =
|
||||||
|
FutureProvider<List<SpotubeArtistObject>>((ref) async {
|
||||||
|
final metadataApi = await ref.watch(metadataPluginApiProvider.future);
|
||||||
|
ref.watch(metadataAuthenticatedProvider);
|
||||||
|
|
||||||
|
final userId = await ref.watch(
|
||||||
|
metadataProviderUserProvider.selectAsync((data) => data?.uid),
|
||||||
|
);
|
||||||
|
if (metadataApi == null || userId == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await metadataApi.listUserSavedArtists(userId);
|
||||||
|
|
||||||
|
return res.items as List<SpotubeArtistObject>;
|
||||||
|
});
|
@ -66,7 +66,7 @@ class PluginSetIntervalApi {
|
|||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
print('Exception no clearInterval: $e');
|
print('Exception no clearInterval: $e');
|
||||||
} on Error catch (e) {
|
} on Error catch (e) {
|
||||||
print('Erro no clearInterval: $e');
|
print('Error no clearInterval: $e');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,10 @@ class MetadataApiSignature {
|
|||||||
final PluginSetIntervalApi setIntervalApi;
|
final PluginSetIntervalApi setIntervalApi;
|
||||||
late MetadataSignatureFlags _signatureFlags;
|
late MetadataSignatureFlags _signatureFlags;
|
||||||
|
|
||||||
|
final StreamController<bool> _authenticatedStreamController;
|
||||||
|
|
||||||
|
Stream<bool> get authenticatedStream => _authenticatedStreamController.stream;
|
||||||
|
|
||||||
MetadataSignatureFlags get signatureFlags => _signatureFlags;
|
MetadataSignatureFlags get signatureFlags => _signatureFlags;
|
||||||
|
|
||||||
MetadataApiSignature._(
|
MetadataApiSignature._(
|
||||||
@ -46,10 +50,19 @@ class MetadataApiSignature {
|
|||||||
this.webViewApi,
|
this.webViewApi,
|
||||||
this.totpGenerator,
|
this.totpGenerator,
|
||||||
this.setIntervalApi,
|
this.setIntervalApi,
|
||||||
);
|
) : _authenticatedStreamController = StreamController<bool>.broadcast() {
|
||||||
|
runtime.onMessage("authenticatedStatus", (args) {
|
||||||
|
if (args[0] is Map && (args[0] as Map).containsKey("authenticated")) {
|
||||||
|
final authenticated = args[0]["authenticated"] as bool;
|
||||||
|
_authenticatedStreamController.add(authenticated);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static Future<MetadataApiSignature> init(
|
static Future<MetadataApiSignature> init(
|
||||||
String libraryCode, PluginConfiguration config) async {
|
String libraryCode,
|
||||||
|
PluginConfiguration config,
|
||||||
|
) async {
|
||||||
final runtime = getJavascriptRuntime(xhr: true).enableXhr();
|
final runtime = getJavascriptRuntime(xhr: true).enableXhr();
|
||||||
runtime.enableHandlePromises();
|
runtime.enableHandlePromises();
|
||||||
await runtime.enableFetch();
|
await runtime.enableFetch();
|
||||||
@ -106,6 +119,7 @@ class MetadataApiSignature {
|
|||||||
Future invoke(String method, [List? args]) async {
|
Future invoke(String method, [List? args]) async {
|
||||||
final completer = Completer();
|
final completer = Completer();
|
||||||
runtime.onMessage(method, (result) {
|
runtime.onMessage(method, (result) {
|
||||||
|
if (completer.isCompleted) return;
|
||||||
try {
|
try {
|
||||||
if (result is Map && result.containsKey("error")) {
|
if (result is Map && result.containsKey("error")) {
|
||||||
completer.completeError(result["error"]);
|
completer.completeError(result["error"]);
|
||||||
@ -113,7 +127,10 @@ class MetadataApiSignature {
|
|||||||
completer.complete(result is String ? jsonDecode(result) : result);
|
completer.complete(result is String ? jsonDecode(result) : result);
|
||||||
}
|
}
|
||||||
} catch (e, stack) {
|
} catch (e, stack) {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(
|
||||||
|
"[MetadataApiSignature][invoke] Error in $method: $e",
|
||||||
|
stack,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final code = """
|
final code = """
|
||||||
@ -157,6 +174,11 @@ class MetadataApiSignature {
|
|||||||
await invoke("metadataApi.authenticate");
|
await invoke("metadataApi.authenticate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isAuthenticated() async {
|
||||||
|
final res = await invoke("metadataApi.isAuthenticated");
|
||||||
|
return res as bool;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
await invoke("metadataApi.logout");
|
await invoke("metadataApi.logout");
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
@ -143,6 +143,7 @@ dependencies:
|
|||||||
otp_util: ^1.0.2
|
otp_util: ^1.0.2
|
||||||
dio_http2_adapter: ^2.6.0
|
dio_http2_adapter: ^2.6.0
|
||||||
flutter_js: ^0.8.2
|
flutter_js: ^0.8.2
|
||||||
|
archive: ^4.0.7
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.13
|
build_runner: ^2.4.13
|
||||||
|
@ -8,6 +8,7 @@ import 'schema_v5.dart' as v5;
|
|||||||
import 'schema_v6.dart' as v6;
|
import 'schema_v6.dart' as v6;
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v7.dart' as v7;
|
||||||
import 'schema_v4.dart' as v4;
|
import 'schema_v4.dart' as v4;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@ -24,6 +25,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 7:
|
||||||
|
return v7.DatabaseAtV7(db);
|
||||||
case 4:
|
case 4:
|
||||||
return v4.DatabaseAtV4(db);
|
return v4.DatabaseAtV4(db);
|
||||||
default:
|
default:
|
||||||
@ -31,5 +34,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7];
|
||||||
}
|
}
|
||||||
|
3758
test/drift/app_db/generated/schema_v7.dart
Normal file
3758
test/drift/app_db/generated/schema_v7.dart
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user