diff --git a/.vscode/launch.json b/.vscode/launch.json index deabf1d3..b81e2eee 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/lib/pages/settings/metadata/metadata_form.dart b/lib/pages/settings/metadata/metadata_form.dart index 7ddddfb9..9887a7e6 100644 --- a/lib/pages/settings/metadata/metadata_form.dart +++ b/lib/pages/settings/metadata/metadata_form.dart @@ -25,122 +25,127 @@ class SettingsMetadataProviderFormPage extends HookConsumerWidget { Widget build(BuildContext context, ref) { final formKey = useMemoized(() => GlobalKey(), []); - 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) => { - "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) => { + "id": e.key, + "value": e.value, + }) + .toList(); + + context.router.maybePop(data); + }, + child: Text(context.l10n.submit), + ), + ), + const SliverGap(200) + ], + ), ), ), ), diff --git a/lib/pages/settings/metadata_plugins.dart b/lib/pages/settings/metadata_plugins.dart index 9609d8f5..96526d05 100644 --- a/lib/pages/settings/metadata_plugins.dart +++ b/lib/pages/settings/metadata_plugins.dart @@ -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, + ], + ), ), ), ), ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/provider/metadata_plugin/album/releases.dart b/lib/provider/metadata_plugin/album/releases.dart index 0d557d0a..e6e88baf 100644 --- a/lib/provider/metadata_plugin/album/releases.dart +++ b/lib/provider/metadata_plugin/album/releases.dart @@ -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); } } diff --git a/lib/provider/metadata_plugin/browse/section_items.dart b/lib/provider/metadata_plugin/browse/section_items.dart index 542db00e..5c03ec2c 100644 --- a/lib/provider/metadata_plugin/browse/section_items.dart +++ b/lib/provider/metadata_plugin/browse/section_items.dart @@ -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); } } diff --git a/lib/provider/metadata_plugin/browse/sections.dart b/lib/provider/metadata_plugin/browse/sections.dart index 957daa86..1f73e10c 100644 --- a/lib/provider/metadata_plugin/browse/sections.dart +++ b/lib/provider/metadata_plugin/browse/sections.dart @@ -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); } } diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 3f45901b..81c4bfe4 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -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(); }