mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
chore: SafeArea
This commit is contained in:
parent
0a604a9ad5
commit
92dde7286f
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@ -17,6 +17,17 @@
|
||||
"dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "spotube (mobile-skia)",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"dev",
|
||||
"--no-enable-impeller"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "spotube (profile)",
|
||||
"type": "dart",
|
||||
|
@ -25,122 +25,127 @@ class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||
|
||||
return Scaffold(
|
||||
headers: [
|
||||
TitleBar(
|
||||
title: Text(title),
|
||||
),
|
||||
],
|
||||
child: FormBuilder(
|
||||
key: formKey,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.theme.typography.h2,
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Scaffold(
|
||||
headers: [
|
||||
TitleBar(
|
||||
title: Text(title),
|
||||
),
|
||||
],
|
||||
child: FormBuilder(
|
||||
key: formKey,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.theme.typography.h2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(24),
|
||||
SliverList.separated(
|
||||
itemCount: fields.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
if (fields[index] is MetadataFormFieldTextObject) {
|
||||
final field =
|
||||
fields[index] as MetadataFormFieldTextObject;
|
||||
return MarkdownBody(
|
||||
data: field.text,
|
||||
onTapLink: (text, href, title) {
|
||||
// TODO: Confirm link opening behavior
|
||||
if (href != null) {
|
||||
launchUrlString(href);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final field = fields[index] as MetadataFormFieldInputObject;
|
||||
return FormBuilderField(
|
||||
name: field.id,
|
||||
initialValue: field.defaultValue,
|
||||
validator: FormBuilderValidators.compose([
|
||||
if (field.required == true)
|
||||
FormBuilderValidators.required(
|
||||
errorText: 'This field is required',
|
||||
),
|
||||
if (field.regex != null)
|
||||
FormBuilderValidators.match(
|
||||
RegExp(field.regex!),
|
||||
errorText:
|
||||
"Input doesn't match the required format",
|
||||
),
|
||||
]),
|
||||
builder: (formField) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
TextField(
|
||||
placeholder: field.placeholder == null
|
||||
? null
|
||||
: Text(field.placeholder!),
|
||||
initialValue: formField.value,
|
||||
onChanged: (value) {
|
||||
formField.didChange(value);
|
||||
},
|
||||
obscureText:
|
||||
field.variant == FormFieldVariant.password,
|
||||
keyboardType:
|
||||
field.variant == FormFieldVariant.number
|
||||
? TextInputType.number
|
||||
: TextInputType.text,
|
||||
features: [
|
||||
if (field.variant == FormFieldVariant.password)
|
||||
const InputFeature.passwordToggle(),
|
||||
],
|
||||
),
|
||||
if (formField.hasError)
|
||||
Text(
|
||||
formField.errorText ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
const SliverGap(24),
|
||||
SliverList.separated(
|
||||
itemCount: fields.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
if (fields[index] is MetadataFormFieldTextObject) {
|
||||
final field =
|
||||
fields[index] as MetadataFormFieldTextObject;
|
||||
return MarkdownBody(
|
||||
data: field.text,
|
||||
onTapLink: (text, href, title) {
|
||||
// TODO: Confirm link opening behavior
|
||||
if (href != null) {
|
||||
launchUrlString(href);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(24),
|
||||
SliverToBoxAdapter(
|
||||
child: Button.primary(
|
||||
onPressed: () {
|
||||
if (formKey.currentState?.saveAndValidate() != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final data = formKey.currentState!.value.entries
|
||||
.map((e) => <String, dynamic>{
|
||||
"id": e.key,
|
||||
"value": e.value,
|
||||
})
|
||||
.toList();
|
||||
|
||||
context.router.maybePop(data);
|
||||
final field =
|
||||
fields[index] as MetadataFormFieldInputObject;
|
||||
return FormBuilderField(
|
||||
name: field.id,
|
||||
initialValue: field.defaultValue,
|
||||
validator: FormBuilderValidators.compose([
|
||||
if (field.required == true)
|
||||
FormBuilderValidators.required(
|
||||
errorText: 'This field is required',
|
||||
),
|
||||
if (field.regex != null)
|
||||
FormBuilderValidators.match(
|
||||
RegExp(field.regex!),
|
||||
errorText:
|
||||
"Input doesn't match the required format",
|
||||
),
|
||||
]),
|
||||
builder: (formField) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
TextField(
|
||||
placeholder: field.placeholder == null
|
||||
? null
|
||||
: Text(field.placeholder!),
|
||||
initialValue: formField.value,
|
||||
onChanged: (value) {
|
||||
formField.didChange(value);
|
||||
},
|
||||
obscureText:
|
||||
field.variant == FormFieldVariant.password,
|
||||
keyboardType:
|
||||
field.variant == FormFieldVariant.number
|
||||
? TextInputType.number
|
||||
: TextInputType.text,
|
||||
features: [
|
||||
if (field.variant ==
|
||||
FormFieldVariant.password)
|
||||
const InputFeature.passwordToggle(),
|
||||
],
|
||||
),
|
||||
if (formField.hasError)
|
||||
Text(
|
||||
formField.errorText ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.red, fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(context.l10n.submit),
|
||||
),
|
||||
),
|
||||
const SliverGap(200)
|
||||
],
|
||||
const SliverGap(24),
|
||||
SliverToBoxAdapter(
|
||||
child: Button.primary(
|
||||
onPressed: () {
|
||||
if (formKey.currentState?.saveAndValidate() != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final data = formKey.currentState!.value.entries
|
||||
.map((e) => <String, dynamic>{
|
||||
"id": e.key,
|
||||
"value": e.value,
|
||||
})
|
||||
.toList();
|
||||
|
||||
context.router.maybePop(data);
|
||||
},
|
||||
child: Text(context.l10n.submit),
|
||||
),
|
||||
),
|
||||
const SliverGap(200)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -52,189 +52,194 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
|
||||
[plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value],
|
||||
);
|
||||
|
||||
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",
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: 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;
|
||||
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);
|
||||
if (url.isNotEmpty) {
|
||||
final pluginConfig = await pluginsNotifier
|
||||
.downloadAndCachePlugin(url);
|
||||
|
||||
await pluginsNotifier.addPlugin(pluginConfig);
|
||||
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: kIsAndroid ? FileType.any : FileType.custom,
|
||||
allowedExtensions: kIsAndroid ? [] : ["smplug"],
|
||||
withData: true,
|
||||
);
|
||||
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: kIsAndroid ? FileType.any : FileType.custom,
|
||||
allowedExtensions: kIsAndroid ? [] : ["smplug"],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
if (result == null) return;
|
||||
|
||||
final file = result.files.first;
|
||||
final file = result.files.first;
|
||||
|
||||
if (file.bytes == null) return;
|
||||
if (file.bytes == null) return;
|
||||
|
||||
final pluginConfig = await pluginsNotifier
|
||||
.extractPluginArchive(file.bytes!);
|
||||
await pluginsNotifier.addPlugin(pluginConfig);
|
||||
},
|
||||
final pluginConfig = await pluginsNotifier
|
||||
.extractPluginArchive(file.bytes!);
|
||||
await pluginsNotifier.addPlugin(pluginConfig);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
if (plugins.asData?.value.plugins.isNotEmpty ?? false)
|
||||
const SliverGap(12),
|
||||
if (plugins.asData?.value.plugins.isNotEmpty ?? false)
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text("Installed").h4,
|
||||
const Gap(8),
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(20),
|
||||
SliverList.separated(
|
||||
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final plugin = plugins.asData!.value.plugins[index];
|
||||
final isDefault =
|
||||
plugins.asData!.value.defaultPlugin == index;
|
||||
return MetadataInstalledPluginItem(
|
||||
plugin: plugin,
|
||||
isDefault: isDefault,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text("Installed").h4,
|
||||
const Text("Available plugins").h4,
|
||||
const Gap(8),
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(20),
|
||||
SliverList.separated(
|
||||
itemCount: plugins.asData?.value.plugins.length ?? 0,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final plugin = plugins.asData!.value.plugins[index];
|
||||
final isDefault = plugins.asData!.value.defaultPlugin == index;
|
||||
return MetadataInstalledPluginItem(
|
||||
plugin: plugin,
|
||||
isDefault: isDefault,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(
|
||||
child: Row(
|
||||
children: [
|
||||
const Gap(8),
|
||||
const Text("Available plugins").h4,
|
||||
const Gap(8),
|
||||
const Expanded(child: Divider()),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SliverGap(12),
|
||||
SliverInfiniteList(
|
||||
isLoading: pluginReposSnapshot.isLoading &&
|
||||
!pluginReposSnapshot.isLoadingNextPage,
|
||||
itemCount: pluginRepos.length,
|
||||
onFetchData: pluginReposNotifier.fetchMore,
|
||||
loadingBuilder: (context) {
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: MetadataPluginRepositoryItem(
|
||||
pluginRepo: MetadataPluginRepository(
|
||||
name: "Loading...",
|
||||
description: "Loading...",
|
||||
repoUrl: "",
|
||||
owner: "",
|
||||
const SliverGap(12),
|
||||
SliverInfiniteList(
|
||||
isLoading: pluginReposSnapshot.isLoading &&
|
||||
!pluginReposSnapshot.isLoadingNextPage,
|
||||
itemCount: pluginRepos.length,
|
||||
onFetchData: pluginReposNotifier.fetchMore,
|
||||
loadingBuilder: (context) {
|
||||
return Skeletonizer(
|
||||
enabled: true,
|
||||
child: MetadataPluginRepositoryItem(
|
||||
pluginRepo: MetadataPluginRepository(
|
||||
name: "Loading...",
|
||||
description: "Loading...",
|
||||
repoUrl: "",
|
||||
owner: "",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final pluginRepo = pluginRepos[index];
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final pluginRepo = pluginRepos[index];
|
||||
|
||||
return MetadataPluginRepositoryItem(
|
||||
pluginRepo: pluginRepo,
|
||||
);
|
||||
},
|
||||
),
|
||||
SliverCrossAxisConstrained(
|
||||
maxCrossAxisExtent: 720,
|
||||
child: SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
child: SafeArea(
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.warning, size: 16),
|
||||
const Text(
|
||||
"Disclaimer",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).bold,
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
"The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\n"
|
||||
"Please use them at your own risk. For any bugs/issues, please report them to the plugin repository."
|
||||
"\n\n"
|
||||
"If any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, "
|
||||
"please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. "
|
||||
"Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, "
|
||||
"so we cannot take any action on them.\n\n",
|
||||
).muted.xSmall,
|
||||
],
|
||||
return MetadataPluginRepositoryItem(
|
||||
pluginRepo: pluginRepo,
|
||||
);
|
||||
},
|
||||
),
|
||||
SliverCrossAxisConstrained(
|
||||
maxCrossAxisExtent: 720,
|
||||
child: SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
child: SafeArea(
|
||||
child: Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 12,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(SpotubeIcons.warning, size: 16),
|
||||
const Text(
|
||||
"Disclaimer",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
).bold,
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
"The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\n"
|
||||
"Please use them at your own risk. For any bugs/issues, please report them to the plugin repository."
|
||||
"\n\n"
|
||||
"If any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, "
|
||||
"please ask the \"Third-party\" plugin author or the hosting platform .e.g GitHub/Codeberg to take action. "
|
||||
"Above listed (\"Third-party\" labelled) are all public/community maintained plugins. We're not curating them, "
|
||||
"so we cannot take any action on them.\n\n",
|
||||
).muted.xSmall,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
|
||||
class MetadataPluginAlbumReleasesNotifier
|
||||
@ -17,7 +17,7 @@ class MetadataPluginAlbumReleasesNotifier
|
||||
|
||||
@override
|
||||
build() async {
|
||||
ref.watch(metadataPluginProvider);
|
||||
ref.watch(metadataPluginAuthenticatedProvider);
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
|
||||
|
||||
class MetadataPluginBrowseSectionItemsNotifier
|
||||
@ -19,7 +19,7 @@ class MetadataPluginBrowseSectionItemsNotifier
|
||||
|
||||
@override
|
||||
build(arg) async {
|
||||
ref.watch(metadataPluginProvider);
|
||||
ref.watch(metadataPluginAuthenticatedProvider);
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/models/metadata/metadata.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||
import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
|
||||
|
||||
class MetadataPluginBrowseSectionsNotifier
|
||||
@ -19,7 +19,7 @@ class MetadataPluginBrowseSectionsNotifier
|
||||
|
||||
@override
|
||||
build() async {
|
||||
ref.watch(metadataPluginProvider);
|
||||
ref.watch(metadataPluginAuthenticatedProvider);
|
||||
return await fetch(0, 20);
|
||||
}
|
||||
}
|
||||
|
@ -69,9 +69,8 @@ abstract class ServiceUtils {
|
||||
}
|
||||
|
||||
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ')
|
||||
.replaceAll(RegExp(r"\sfeat\.|\sft\."), ' ')
|
||||
.replaceAll(RegExp(r"\sfeat\.|\sft\.", caseSensitive: false), ' ')
|
||||
.replaceAll(RegExp(r"\s+"), ' ')
|
||||
.trim();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user