feat: custom piped & invidious instance support

This commit is contained in:
Kingkor Roy Tirtho 2025-03-07 11:50:39 +06:00
parent c3bbc129ad
commit 91871d0d26
4 changed files with 764 additions and 500 deletions

View File

@ -8,6 +8,7 @@ class AdaptiveSelectTile<T> extends HookWidget {
final Widget title;
final Widget? subtitle;
final Widget? secondary;
final List<Widget>? trailing;
final ListTileControlAffinity? controlAffinity;
final T value;
final ValueChanged<T?>? onChanged;
@ -34,6 +35,7 @@ class AdaptiveSelectTile<T> extends HookWidget {
this.controlAffinity = ListTileControlAffinity.trailing,
this.subtitle,
this.secondary,
this.trailing,
this.breakLayout,
this.showValueWhenUnfolded = true,
super.key,
@ -54,8 +56,10 @@ class AdaptiveSelectTile<T> extends HookWidget {
onChanged: onChanged,
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
autoClosePopover: true,
popup: (context) {
return SelectPopup(
autoClose: true,
items: SelectItemBuilder(
childCount: options.length,
builder: (context, index) {
@ -82,9 +86,20 @@ class AdaptiveSelectTile<T> extends HookWidget {
leading: controlAffinity != ListTileControlAffinity.leading
? secondary
: control,
trailing: controlAffinity == ListTileControlAffinity.leading
? secondary
: control,
trailing: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
spacing: 5,
children: [
...?trailing,
if (controlAffinity == ListTileControlAffinity.leading &&
secondary != null)
secondary!
else if (controlAffinity == ListTileControlAffinity.trailing &&
control != null)
control,
],
),
onTap: breakLayout ?? mediaQuery.mdAndUp
? null
: () {

View File

@ -422,5 +422,7 @@
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
"download": "Download",
"file_not_found": "File not found"
"file_not_found": "File not found",
"custom": "Custom",
"add_custom_url": "Add custom URL"
}

View File

@ -4,6 +4,9 @@ import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show ListTile;
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:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -11,6 +14,8 @@ import 'package:piped_client/piped_client.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
@ -97,10 +102,106 @@ class SettingsPlaybackSection extends HookConsumerWidget {
),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => SelectItemButton(
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
),
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) => HookBuilder(
builder: (context) {
final controller =
useShadcnTextEditingController(
text: preferences.pipedInstance,
);
final formKey = useMemoized(
() => GlobalKey<FormBuilderState>(), []);
return Alert(
title:
Text(context.l10n.piped_instance).h4(),
content: FormBuilder(
key: formKey,
child: Column(
children: [
const Gap(10),
TextFormBuilderField(
name: "url",
controller: controller,
placeholder: Text(
context.l10n.piped_instance),
validator:
FormBuilderValidators.url(),
),
const Gap(10),
Row(
children: [
Expanded(
child: Button.secondary(
onPressed: () {
Navigator.of(context).pop();
},
child:
Text(context.l10n.cancel),
),
),
const Gap(10),
Expanded(
child: Button.primary(
onPressed: () {
if (!formKey.currentState!
.saveAndValidate()) {
return;
}
preferencesNotifier
.setPipedInstance(
controller.text,
);
Navigator.of(context).pop();
},
child:
Text(context.l10n.save),
),
),
],
)
],
),
),
);
},
),
);
},
),
)
],
options: [
if (data
.none((e) => e.apiUrl == preferences.pipedInstance))
SelectItemButton(
value: preferences.pipedInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.pipedInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.apiUrl,
child: RichText(
text: TextSpan(
@ -121,8 +222,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
),
),
),
)
.toList(),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setPipedInstance(value);
@ -157,12 +257,108 @@ class SettingsPlaybackSection extends HookConsumerWidget {
"${context.l10n.invidious_description}\n"
"${context.l10n.invidious_warning}",
),
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
),
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) => HookBuilder(
builder: (context) {
final controller =
useShadcnTextEditingController(
text: preferences.invidiousInstance,
);
final formKey = useMemoized(
() => GlobalKey<FormBuilderState>(), []);
return Alert(
title: Text(context.l10n.invidious_instance)
.h4(),
content: FormBuilder(
key: formKey,
child: Column(
children: [
const Gap(10),
TextFormBuilderField(
name: "url",
controller: controller,
placeholder: Text(context
.l10n.invidious_instance),
validator:
FormBuilderValidators.url(),
),
const Gap(10),
Row(
children: [
Expanded(
child: Button.secondary(
onPressed: () {
Navigator.of(context).pop();
},
child:
Text(context.l10n.cancel),
),
),
const Gap(10),
Expanded(
child: Button.primary(
onPressed: () {
if (!formKey.currentState!
.saveAndValidate()) {
return;
}
preferencesNotifier
.setInvidiousInstance(
controller.text,
);
Navigator.of(context).pop();
},
child:
Text(context.l10n.save),
),
),
],
)
],
),
),
);
},
),
);
},
),
)
],
value: preferences.invidiousInstance,
showValueWhenUnfolded: false,
options: data
.sortedBy((e) => e.name)
.map(
(e) => SelectItemButton(
options: [
if (data.none((e) =>
e.details.uri == preferences.invidiousInstance))
SelectItemButton(
value: preferences.invidiousInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.invidiousInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.details.uri,
child: RichText(
text: TextSpan(
@ -183,8 +379,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
),
),
),
)
.toList(),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setInvidiousInstance(value);

View File

@ -22,7 +22,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"bn": [
@ -48,7 +50,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ca": [
@ -74,7 +78,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"cs": [
@ -100,7 +106,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"de": [
@ -126,7 +134,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"es": [
@ -152,7 +162,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"eu": [
@ -178,7 +190,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"fa": [
@ -204,7 +218,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"fi": [
@ -230,7 +246,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"fr": [
@ -256,7 +274,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"hi": [
@ -282,7 +302,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"id": [
@ -308,7 +330,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"it": [
@ -334,7 +358,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ja": [
@ -360,7 +386,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ka": [
@ -386,7 +414,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ko": [
@ -412,7 +442,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ne": [
@ -438,7 +470,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"nl": [
@ -464,7 +498,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"pl": [
@ -490,7 +526,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"pt": [
@ -516,7 +554,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"ru": [
@ -542,7 +582,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"th": [
@ -568,7 +610,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"tr": [
@ -594,7 +638,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"uk": [
@ -620,7 +666,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"vi": [
@ -646,7 +694,9 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
],
"zh": [
@ -672,6 +722,8 @@
"youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
"file_not_found",
"custom",
"add_custom_url"
]
}