chore: SafeArea

This commit is contained in:
Kingkor Roy Tirtho 2025-07-20 21:22:37 +06:00
parent 0a604a9ad5
commit 92dde7286f
7 changed files with 291 additions and 271 deletions

11
.vscode/launch.json vendored
View File

@ -17,6 +17,17 @@
"dev" "dev"
] ]
}, },
{
"name": "spotube (mobile-skia)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"args": [
"--flavor",
"dev",
"--no-enable-impeller"
]
},
{ {
"name": "spotube (profile)", "name": "spotube (profile)",
"type": "dart", "type": "dart",

View File

@ -25,122 +25,127 @@ class SettingsMetadataProviderFormPage extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []); final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
return Scaffold( return SafeArea(
headers: [ bottom: false,
TitleBar( child: Scaffold(
title: Text(title), headers: [
), TitleBar(
], title: Text(title),
child: FormBuilder( ),
key: formKey, ],
child: Center( child: FormBuilder(
child: Container( key: formKey,
padding: const EdgeInsets.all(16), child: Center(
constraints: const BoxConstraints(maxWidth: 600), child: Container(
child: CustomScrollView( padding: const EdgeInsets.all(16),
shrinkWrap: true, constraints: const BoxConstraints(maxWidth: 600),
slivers: [ child: CustomScrollView(
SliverToBoxAdapter( shrinkWrap: true,
child: Text( slivers: [
title, SliverToBoxAdapter(
textAlign: TextAlign.center, child: Text(
style: context.theme.typography.h2, title,
textAlign: TextAlign.center,
style: context.theme.typography.h2,
),
), ),
), const SliverGap(24),
const SliverGap(24), SliverList.separated(
SliverList.separated( itemCount: fields.length,
itemCount: fields.length, separatorBuilder: (context, index) => const Gap(12),
separatorBuilder: (context, index) => const Gap(12), itemBuilder: (context, index) {
itemBuilder: (context, index) { if (fields[index] is MetadataFormFieldTextObject) {
if (fields[index] is MetadataFormFieldTextObject) { final field =
final field = fields[index] as MetadataFormFieldTextObject;
fields[index] as MetadataFormFieldTextObject; return MarkdownBody(
return MarkdownBody( data: field.text,
data: field.text, onTapLink: (text, href, title) {
onTapLink: (text, href, title) { // TODO: Confirm link opening behavior
// TODO: Confirm link opening behavior if (href != null) {
if (href != null) { launchUrlString(href);
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),
SliverToBoxAdapter(
child: Button.primary(
onPressed: () {
if (formKey.currentState?.saveAndValidate() != true) {
return;
} }
final data = formKey.currentState!.value.entries final field =
.map((e) => <String, dynamic>{ fields[index] as MetadataFormFieldInputObject;
"id": e.key, return FormBuilderField(
"value": e.value, name: field.id,
}) initialValue: field.defaultValue,
.toList(); validator: FormBuilderValidators.compose([
if (field.required == true)
context.router.maybePop(data); 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(24),
const SliverGap(200) 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)
],
),
), ),
), ),
), ),

View File

@ -52,189 +52,194 @@ class SettingsMetadataProviderPage extends HookConsumerWidget {
[plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value], [plugins.asData?.value.plugins, pluginReposSnapshot.asData?.value],
); );
return Scaffold( return SafeArea(
headers: const [ bottom: false,
TitleBar( child: Scaffold(
title: Text("Metadata provider plugin"), headers: const [
) TitleBar(
], title: Text("Metadata provider plugin"),
child: Padding( )
padding: const EdgeInsets.all(8), ],
child: CustomScrollView( child: Padding(
slivers: [ padding: const EdgeInsets.all(8),
SliverToBoxAdapter( child: CustomScrollView(
child: Row( slivers: [
spacing: 8, SliverToBoxAdapter(
children: [ child: Row(
Expanded( spacing: 8,
child: FormBuilder( children: [
key: formKey, Expanded(
child: TextFormBuilderField( child: FormBuilder(
name: "plugin_url", key: formKey,
validator: FormBuilderValidators.url( child: TextFormBuilderField(
protocols: ["http", "https"]), name: "plugin_url",
placeholder: const Text( validator: FormBuilderValidators.url(
"Add GitHub/Codeberg URL to plugin repository " protocols: ["http", "https"]),
"or direct link to .smplug file", placeholder: const Text(
"Add GitHub/Codeberg URL to plugin repository "
"or direct link to .smplug file",
),
), ),
), ),
), ),
), Tooltip(
Tooltip( tooltip: const TooltipContainer(
tooltip: const TooltipContainer( child: Text("Download and install plugin from url"),
child: Text("Download and install plugin from url"), ).call,
).call, child: IconButton.secondary(
child: IconButton.secondary( icon: const Icon(SpotubeIcons.download),
icon: const Icon(SpotubeIcons.download), onPressed: () async {
onPressed: () async { if (formKey.currentState?.saveAndValidate() ??
if (formKey.currentState?.saveAndValidate() ?? false) { false) {
final url = formKey.currentState?.fields["plugin_url"] final url = formKey.currentState
?.value as String; ?.fields["plugin_url"]?.value as String;
if (url.isNotEmpty) { if (url.isNotEmpty) {
final pluginConfig = await pluginsNotifier final pluginConfig = await pluginsNotifier
.downloadAndCachePlugin(url); .downloadAndCachePlugin(url);
await pluginsNotifier.addPlugin(pluginConfig); await pluginsNotifier.addPlugin(pluginConfig);
}
} }
} },
}, ),
), ),
), Tooltip(
Tooltip( tooltip: const TooltipContainer(
tooltip: const TooltipContainer( child: Text("Upload plugin from file"),
child: Text("Upload plugin from file"), ).call,
).call, child: IconButton.primary(
child: IconButton.primary( icon: const Icon(SpotubeIcons.upload),
icon: const Icon(SpotubeIcons.upload), onPressed: () async {
onPressed: () async { final result = await FilePicker.platform.pickFiles(
final result = await FilePicker.platform.pickFiles( type: kIsAndroid ? FileType.any : FileType.custom,
type: kIsAndroid ? FileType.any : FileType.custom, allowedExtensions: kIsAndroid ? [] : ["smplug"],
allowedExtensions: kIsAndroid ? [] : ["smplug"], withData: true,
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 final pluginConfig = await pluginsNotifier
.extractPluginArchive(file.bytes!); .extractPluginArchive(file.bytes!);
await pluginsNotifier.addPlugin(pluginConfig); await pluginsNotifier.addPlugin(pluginConfig);
}, },
),
), ),
), ],
], ),
), ),
), const SliverGap(12),
const SliverGap(12), if (plugins.asData?.value.plugins.isNotEmpty ?? false)
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( SliverToBoxAdapter(
child: Row( child: Row(
children: [ children: [
const Gap(8), const Gap(8),
const Text("Installed").h4, const Text("Available plugins").h4,
const Gap(8), const Gap(8),
const Expanded(child: Divider()), const Expanded(child: Divider()),
const Gap(8), const Gap(8),
], ],
), ),
), ),
const SliverGap(20), const SliverGap(12),
SliverList.separated( SliverInfiniteList(
itemCount: plugins.asData?.value.plugins.length ?? 0, isLoading: pluginReposSnapshot.isLoading &&
separatorBuilder: (context, index) => const Gap(12), !pluginReposSnapshot.isLoadingNextPage,
itemBuilder: (context, index) { itemCount: pluginRepos.length,
final plugin = plugins.asData!.value.plugins[index]; onFetchData: pluginReposNotifier.fetchMore,
final isDefault = plugins.asData!.value.defaultPlugin == index; loadingBuilder: (context) {
return MetadataInstalledPluginItem( return Skeletonizer(
plugin: plugin, enabled: true,
isDefault: isDefault, child: MetadataPluginRepositoryItem(
); pluginRepo: MetadataPluginRepository(
}, name: "Loading...",
), description: "Loading...",
const SliverGap(12), repoUrl: "",
SliverToBoxAdapter( owner: "",
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: "",
), ),
), );
); },
}, itemBuilder: (context, index) {
itemBuilder: (context, index) { final pluginRepo = pluginRepos[index];
final pluginRepo = pluginRepos[index];
return MetadataPluginRepositoryItem( return MetadataPluginRepositoryItem(
pluginRepo: pluginRepo, pluginRepo: pluginRepo,
); );
}, },
), ),
SliverCrossAxisConstrained( SliverCrossAxisConstrained(
maxCrossAxisExtent: 720, maxCrossAxisExtent: 720,
child: SliverFillRemaining( child: SliverFillRemaining(
hasScrollBody: false, hasScrollBody: false,
child: Container( child: Container(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
child: SafeArea( child: SafeArea(
child: Card( child: Card(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
spacing: 12, spacing: 12,
children: [ children: [
Row( Row(
spacing: 8, spacing: 8,
children: [ children: [
const Icon(SpotubeIcons.warning, size: 16), const Icon(SpotubeIcons.warning, size: 16),
const Text( const Text(
"Disclaimer", "Disclaimer",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
).bold, ).bold,
], ],
), ),
const Text( const Text(
"The Spotube team does not hold any responsibility (including legal) for any \"Third-party\" plugins.\n" "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." "Please use them at your own risk. For any bugs/issues, please report them to the plugin repository."
"\n\n" "\n\n"
"If any \"Third-party\" plugin is breaking ToS/DMCA of any service/legal entity, " "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. " "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, " "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", "so we cannot take any action on them.\n\n",
).muted.xSmall, ).muted.xSmall,
], ],
),
), ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
); );

View File

@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.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'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginAlbumReleasesNotifier class MetadataPluginAlbumReleasesNotifier
@ -17,7 +17,7 @@ class MetadataPluginAlbumReleasesNotifier
@override @override
build() async { build() async {
ref.watch(metadataPluginProvider); ref.watch(metadataPluginAuthenticatedProvider);
return await fetch(0, 20); return await fetch(0, 20);
} }
} }

View File

@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.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'; import 'package:spotube/provider/metadata_plugin/utils/family_paginated.dart';
class MetadataPluginBrowseSectionItemsNotifier class MetadataPluginBrowseSectionItemsNotifier
@ -19,7 +19,7 @@ class MetadataPluginBrowseSectionItemsNotifier
@override @override
build(arg) async { build(arg) async {
ref.watch(metadataPluginProvider); ref.watch(metadataPluginAuthenticatedProvider);
return await fetch(0, 20); return await fetch(0, 20);
} }
} }

View File

@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/metadata/metadata.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'; import 'package:spotube/provider/metadata_plugin/utils/paginated.dart';
class MetadataPluginBrowseSectionsNotifier class MetadataPluginBrowseSectionsNotifier
@ -19,7 +19,7 @@ class MetadataPluginBrowseSectionsNotifier
@override @override
build() async { build() async {
ref.watch(metadataPluginProvider); ref.watch(metadataPluginAuthenticatedProvider);
return await fetch(0, 20); return await fetch(0, 20);
} }
} }

View File

@ -69,9 +69,8 @@ abstract class ServiceUtils {
} }
return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}" return "$title ${artists.map((e) => e.replaceAll(",", " ")).join(", ")}"
.toLowerCase()
.replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ') .replaceAll(RegExp(r"\s*\[[^\]]*]"), ' ')
.replaceAll(RegExp(r"\sfeat\.|\sft\."), ' ') .replaceAll(RegExp(r"\sfeat\.|\sft\.", caseSensitive: false), ' ')
.replaceAll(RegExp(r"\s+"), ' ') .replaceAll(RegExp(r"\s+"), ' ')
.trim(); .trim();
} }