chore: upgrade to latest working shadcn_flutter

This commit is contained in:
Kingkor Roy Tirtho 2025-03-03 22:23:29 +06:00
parent 30bf0bed62
commit 7eb0e69dd7
18 changed files with 360 additions and 172 deletions

View File

@ -54,7 +54,16 @@ class AdaptiveSelectTile<T> extends HookWidget {
onChanged: onChanged,
popupConstraints: popupConstraints ?? const BoxConstraints(maxWidth: 200),
popupWidthConstraint: popupWidthConstraint ?? PopoverConstraint.flexible,
children: options,
popup: (context) {
return SelectPopup(
items: SelectItemBuilder(
childCount: options.length,
builder: (context, index) {
return options[index];
},
),
);
},
);
if (mediaQuery.smAndDown) {

View File

@ -11,9 +11,9 @@ class TextFormBuilderField extends StatelessWidget {
final TextEditingController? controller;
final bool filled;
final Widget? placeholder;
final AlignmentGeometry? placeholderAlignment;
final AlignmentGeometry? leadingAlignment;
final AlignmentGeometry? trailingAlignment;
// final AlignmentGeometry? placeholderAlignment;
// final AlignmentGeometry? leadingAlignment;
// final AlignmentGeometry? trailingAlignment;
final bool border;
final Widget? leading;
final Widget? trailing;
@ -41,9 +41,9 @@ class TextFormBuilderField extends StatelessWidget {
final void Function(PointerDownEvent event)? onTapOutside;
final List<TextInputFormatter>? inputFormatters;
final TextStyle? style;
final EditableTextContextMenuBuilder? contextMenuBuilder;
final bool useNativeContextMenu;
final bool? isCollapsed;
// final EditableTextContextMenuBuilder? contextMenuBuilder;
// final bool useNativeContextMenu;
// final bool? isCollapsed;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final Clip clipBehavior;
@ -86,15 +86,15 @@ class TextFormBuilderField extends StatelessWidget {
this.onTapOutside,
this.inputFormatters,
this.style,
this.contextMenuBuilder = TextField.defaultContextMenuBuilder,
this.useNativeContextMenu = false,
this.isCollapsed,
// this.contextMenuBuilder = TextField.defaultContextMenuBuilder,
// this.useNativeContextMenu = false,
// this.isCollapsed,
this.textInputAction,
this.clipBehavior = Clip.hardEdge,
this.autofocus = false,
this.placeholderAlignment,
this.leadingAlignment,
this.trailingAlignment,
// this.placeholderAlignment,
// this.leadingAlignment,
// this.trailingAlignment,
this.statesController,
});
@ -161,16 +161,16 @@ class TextFormBuilderField extends StatelessWidget {
onTapOutside: onTapOutside,
inputFormatters: inputFormatters,
style: style,
contextMenuBuilder: contextMenuBuilder,
useNativeContextMenu: useNativeContextMenu,
isCollapsed: isCollapsed,
// contextMenuBuilder: contextMenuBuilder,
// useNativeContextMenu: useNativeContextMenu,
// isCollapsed: isCollapsed,
keyboardType: keyboardType,
textInputAction: textInputAction,
clipBehavior: clipBehavior,
autofocus: autofocus,
placeholderAlignment: placeholderAlignment,
leadingAlignment: leadingAlignment,
trailingAlignment: trailingAlignment,
// placeholderAlignment: placeholderAlignment,
// leadingAlignment: leadingAlignment,
// trailingAlignment: trailingAlignment,
statesController: statesController,
),
if (field.hasError)

View File

@ -1,4 +1,3 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
@ -9,6 +8,7 @@ import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
class TrackPresentationModifiersSection extends HookConsumerWidget {
final FocusNode? focusNode;
@ -25,7 +25,7 @@ class TrackPresentationModifiersSection extends HookConsumerWidget {
presentationStateProvider(options.collection).notifier,
);
final controller = useTextEditingController();
final controller = useShadcnTextEditingController();
final scale = context.theme.scaling;
return LayoutBuilder(builder: (context, constrains) {

View File

@ -0,0 +1,97 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
class _TextEditingControllerHookCreator {
const _TextEditingControllerHookCreator();
/// Creates a [TextEditingController] that will be disposed automatically.
///
/// The [text] parameter can be used to set the initial value of the
/// controller.
TextEditingController call({String? text, List<Object?>? keys}) {
return use(_TextEditingControllerHook(text, keys));
}
/// Creates a [TextEditingController] from the initial [value] that will
/// be disposed automatically.
TextEditingController fromValue(
TextEditingValue value, [
List<Object?>? keys,
]) {
return use(_TextEditingControllerHook.fromValue(value, keys));
}
}
/// Creates a [TextEditingController], either via an initial text or an initial
/// [TextEditingValue].
///
/// To use a [TextEditingController] with an optional initial text, use:
/// ```dart
/// final controller = useTextEditingController(text: 'initial text');
/// ```
///
/// To use a [TextEditingController] with an optional initial value, use:
/// ```dart
/// final controller = useTextEditingController
/// .fromValue(TextEditingValue.empty);
/// ```
///
/// Changing the text or initial value after the widget has been built has no
/// effect whatsoever. To update the value in a callback, for instance after a
/// button was pressed, use the [TextEditingController.text] or
/// [TextEditingController.value] setters. To have the [TextEditingController]
/// reflect changing values, you can use [useEffect]. This example will update
/// the [TextEditingController.text] whenever a provided [ValueListenable]
/// changes:
/// ```dart
/// final controller = useTextEditingController();
/// final update = useValueListenable(myTextControllerUpdates);
///
/// useEffect(() {
/// controller.text = update;
/// }, [update]);
/// ```
///
/// See also:
/// - [TextEditingController], which this hook creates.
const useShadcnTextEditingController = _TextEditingControllerHookCreator();
class _TextEditingControllerHook extends Hook<TextEditingController> {
const _TextEditingControllerHook(
this.initialText, [
List<Object?>? keys,
]) : initialValue = null,
super(keys: keys);
const _TextEditingControllerHook.fromValue(
TextEditingValue this.initialValue, [
List<Object?>? keys,
]) : initialText = null,
super(keys: keys);
final String? initialText;
final TextEditingValue? initialValue;
@override
_TextEditingControllerHookState createState() {
return _TextEditingControllerHookState();
}
}
class _TextEditingControllerHookState
extends HookState<TextEditingController, _TextEditingControllerHook> {
late final _controller = hook.initialValue != null
? TextEditingController.fromValue(
hook.initialValue ?? TextEditingValue.empty,
)
: TextEditingController(text: hook.initialText);
@override
TextEditingController build(BuildContext context) => _controller;
@override
void dispose() => _controller.dispose();
@override
String get debugLabel => 'useTextEditingController';
}

View File

@ -1,5 +1,6 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/modules/library/playlist_generate/recommendation_attribute_dials.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
@ -21,10 +22,12 @@ class RecommendationAttributeFields extends HookWidget {
@override
Widget build(BuildContext context) {
final minController = useTextEditingController(text: values.min.toString());
final minController =
useShadcnTextEditingController(text: values.min.toString());
final targetController =
useTextEditingController(text: values.target.toString());
final maxController = useTextEditingController(text: values.max.toString());
useShadcnTextEditingController(text: values.target.toString());
final maxController =
useShadcnTextEditingController(text: values.max.toString());
useEffect(() {
listener() {

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart' show Autocomplete;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
enum SelectedItemDisplayType {
wrap,
@ -49,7 +50,7 @@ class SeedsMultiAutocomplete<T extends Object> extends HookWidget {
useValueListenable(seeds);
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
final seedController = useTextEditingController();
final seedController = useShadcnTextEditingController();
final containerKey = useRef(GlobalKey());

View File

@ -13,6 +13,7 @@ import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
@ -85,7 +86,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
final defaultSearchTerm =
"$title - ${activeTrack?.artists?.asString() ?? ""}";
final searchController = useTextEditingController(
final searchController = useShadcnTextEditingController(
text: defaultSearchTerm,
);

View File

@ -8,6 +8,7 @@ import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart';
@ -28,7 +29,7 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final controller = useTextEditingController();
final controller = useShadcnTextEditingController();
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
return AlertDialog(

View File

@ -63,7 +63,7 @@ class StatsPageSummarySection extends HookConsumerWidget {
description: context.l10n.summary_owed_to_artists,
color: Colors.green,
onTap: () {
context.navigateTo(const StatsStreamsRoute());
context.navigateTo(const StatsStreamFeesRoute());
},
),
SummaryCard(

View File

@ -30,24 +30,30 @@ class StatsPageTopSection extends HookConsumerWidget {
};
final dropdown = Select<HistoryDuration>(
popupConstraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.flexible,
padding: const EdgeInsets.all(4),
borderRadius: BorderRadius.circular(4),
value: historyDuration,
onChanged: (value) {
if (value == null) return;
historyDurationNotifier.update((_) => value);
},
itemBuilder: (context, item) => Text(translations[item]!),
children: [
for (final item in HistoryDuration.values)
SelectItemButton(
value: item,
child: Text(translations[item]!),
),
],
);
popupConstraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.flexible,
padding: const EdgeInsets.all(4),
borderRadius: BorderRadius.circular(4),
value: historyDuration,
onChanged: (value) {
if (value == null) return;
historyDurationNotifier.update((_) => value);
},
itemBuilder: (context, item) => Text(translations[item]!),
popup: (context) {
return SelectPopup(
items: SelectItemBuilder(
childCount: HistoryDuration.values.length,
builder: (context, index) {
final item = HistoryDuration.values[index];
return SelectItemButton(
value: item,
child: Text(translations[item]!),
);
},
),
);
});
return SliverLayoutBuilder(builder: (context, constraints) {
return SliverMainAxisGroup(

View File

@ -84,20 +84,28 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
Text(value.name.capitalize()),
],
),
children: [
for (final source in AudioSource.values)
SelectItemButton(
value: source,
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
audioSourceToIconMap[source]!,
Text(source.name.capitalize()),
],
),
popup: (context) {
return SelectPopup(
items: SelectItemBuilder(
childCount: AudioSource.values.length,
builder: (context, index) {
final source = AudioSource.values[index];
return SelectItemButton(
value: source,
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
audioSourceToIconMap[source]!,
Text(source.name.capitalize()),
],
),
);
},
),
],
);
},
),
const Gap(16),
Text(

View File

@ -14,6 +14,22 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
const GettingStartedPageLanguageRegionSection(
{super.key, required this.onNext});
bool filterMarkets(Market item, String query) {
final market = spotifyMarkets
.firstWhere((element) => element.$1 == item)
.$2
.toLowerCase();
return market.contains(query.toLowerCase());
}
bool filterLocale(Locale locale, String query) {
final language =
LanguageLocals.getDisplayLanguage(locale.languageCode).toString();
return language.toLowerCase().contains(query.toLowerCase());
}
@override
Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider);
@ -62,22 +78,30 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
.firstWhere((element) => element.$1 == value)
.$2,
),
searchPlaceholder: Text(context.l10n.search),
searchFilter: (item, query) {
final market = spotifyMarkets
.firstWhere((element) => element.$1 == item)
.$2
.toLowerCase();
return market.contains(query.toLowerCase()) ? 1 : 0;
},
children: [
for (final market in spotifyMarkets)
SelectItemButton(
value: market.$1,
child: Text(market.$2),
),
],
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.search),
builder: (context, searchQuery) {
final filteredMarkets = searchQuery == null ||
searchQuery.isEmpty
? spotifyMarkets
: spotifyMarkets
.where(
(element) =>
filterMarkets(element.$1, searchQuery),
)
.toList();
return SelectItemBuilder(
childCount: filteredMarkets.length,
builder: (context, index) {
final market = filteredMarkets[index];
return SelectItemButton(
value: market.$1,
child: Text(market.$2),
);
},
);
},
).call,
),
),
const Gap(36),
@ -106,33 +130,46 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
value.languageCode)
.toString(),
),
searchPlaceholder: Text(context.l10n.search),
searchFilter: (locale, query) {
final language = LanguageLocals.getDisplayLanguage(
locale.languageCode)
.toString();
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.search),
builder: (context, searchQuery) {
final filteredLocale = searchQuery?.isNotEmpty != true
? L10n.all
: L10n.all
.where(
(element) =>
filterLocale(element, searchQuery!),
)
.toList();
return language
.toLowerCase()
.contains(query.toLowerCase())
? 1
: 0;
},
children: [
SelectItemButton(
value: const Locale("system", "system"),
child: Text(context.l10n.system_default),
),
for (final locale in L10n.all)
SelectItemButton(
value: locale,
child: Text(
LanguageLocals.getDisplayLanguage(
locale.languageCode)
.toString(),
),
),
],
return SelectItemBuilder(
childCount: filteredLocale.length + 1,
builder: (context, index) {
if (index == 0 &&
searchQuery?.isNotEmpty != true) {
return SelectItemButton(
value: const Locale("system", "system"),
child: Text(context.l10n.system_default),
);
}
final indexThen = searchQuery?.isNotEmpty != true
? index
: index - 1;
final locale = filteredLocale[indexThen];
return SelectItemButton(
value: locale,
child: Text(
LanguageLocals.getDisplayLanguage(
locale.languageCode)
.toString(),
),
);
},
);
},
).call,
),
),
],

View File

@ -193,16 +193,11 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
final genreSelector = MultiSelect<String>(
value: genres.value,
searchFilter: (item, query) {
return item.toLowerCase().contains(query.toLowerCase()) ? 1 : 0;
},
onChanged: (value) {
if (!enabled) return;
genres.value = value;
genres.value = value?.toList() ?? [];
},
itemBuilder: (context, item) => Text(item),
searchPlaceholder: Text(context.l10n.select_genres),
orderSelectedFirst: false,
popoverAlignment: Alignment.bottomCenter,
popupConstraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * .8,
@ -213,13 +208,33 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
context.l10n.genre,
),
),
children: [
for (final option in genresCollection.asData?.value ?? <String>[])
SelectItemButton(
value: option,
child: Text(option),
),
],
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.select_genres),
builder: (context, searchQuery) {
final filteredGenres = searchQuery?.isNotEmpty != true
? genresCollection.asData?.value ?? []
: genresCollection.asData?.value
.where(
(item) => item
.toLowerCase()
.contains(searchQuery!.toLowerCase()),
)
.toList() ??
[];
return SelectItemBuilder(
childCount: filteredGenres.length,
builder: (context, index) {
final option = filteredGenres[index];
return SelectItemButton(
value: option,
child: Text(option),
);
},
);
},
).call,
);
final countrySelector = ValueListenableBuilder(
@ -231,25 +246,35 @@ class PlaylistGeneratorPage extends HookConsumerWidget {
onChanged: (value) {
market.value = value!;
},
searchFilter: (item, query) {
return item.name.toLowerCase().contains(query.toLowerCase())
? 1
: 0;
},
searchPlaceholder: Text(context.l10n.search),
popupConstraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * .8,
),
popoverAlignment: Alignment.bottomCenter,
itemBuilder: (context, value) => Text(value.name),
children: spotifyMarkets
.map(
(country) => SelectItemButton(
value: country.$1,
child: Text(country.$2),
),
)
.toList(),
popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.search),
builder: (context, searchQuery) {
final filteredMarkets = searchQuery == null || searchQuery.isEmpty
? spotifyMarkets
: spotifyMarkets
.where(
(item) => item.$1.name
.toLowerCase()
.contains(searchQuery.toLowerCase()),
)
.toList();
return SelectItemBuilder(
childCount: filteredMarkets.length,
builder: (context, index) {
return SelectItemButton(
value: filteredMarkets[index].$1,
child: Text(filteredMarkets[index].$2),
);
},
);
},
).call,
);
},
);

View File

@ -16,6 +16,7 @@ import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart';
@ -79,7 +80,7 @@ class LocalLibraryPage extends HookConsumerWidget {
final isPlaylistPlaying = playlist.containsTracks(
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
final searchController = useTextEditingController();
final searchController = useShadcnTextEditingController();
useValueListenable(searchController);
final searchFocus = useFocusNode();
final isFiltering = useState(false);

View File

@ -14,6 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/pages/search/sections/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart';
@ -35,7 +36,7 @@ class SearchPage extends HookConsumerWidget {
final mediaQuery = MediaQuery.sizeOf(context);
final scrollController = useScrollController();
final controller = useSearchController();
final controller = useShadcnTextEditingController();
final focusNode = useFocusNode();
final auth = ref.watch(authenticationProvider);
@ -120,41 +121,34 @@ class SearchPage extends HookConsumerWidget {
}
},
child: AutoComplete(
autofocus: true,
controller: controller,
suggestions: suggestions,
leading: const Icon(SpotubeIcons.search),
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
trailing: AnimatedCrossFade(
duration:
const Duration(milliseconds: 300),
crossFadeState:
controller.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon: const Icon(SpotubeIcons.close),
onPressed: () {
controller.clear();
},
child: TextField(
autofocus: true,
controller: controller,
leading:
const Icon(SpotubeIcons.search),
textInputAction: TextInputAction.search,
placeholder: Text(context.l10n.search),
trailing: AnimatedCrossFade(
duration:
const Duration(milliseconds: 300),
crossFadeState:
controller.text.isNotEmpty
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton.ghost(
size: ButtonSize.small,
icon:
const Icon(SpotubeIcons.close),
onPressed: () {
controller.clear();
},
),
secondChild: const SizedBox.square(
dimension: 28),
),
secondChild: const SizedBox.square(
dimension: 28),
onSubmitted: onSubmitted,
),
onAcceptSuggestion: (index) {
controller.text = KVStoreService
.recentSearches[index];
ref
.read(searchTermStateProvider
.notifier)
.state =
KVStoreService
.recentSearches[index];
},
onChanged: (value) {},
onSubmitted: onSubmitted,
),
);
}),

View File

@ -91,13 +91,18 @@ class StatsStreamFeesPage extends HookConsumerWidget {
Text(translations[value]!),
constraints: const BoxConstraints(maxWidth: 150),
popupWidthConstraint: PopoverConstraint.anchorMaxSize,
children: [
for (final entry in translations.entries)
SelectItemButton(
value: entry.key,
child: Text(entry.value),
),
],
popup: SelectPopup(
items: SelectItemBuilder(
childCount: translations.length,
builder: (context, index) {
final entry = translations.entries.elementAt(index);
return SelectItemButton(
value: entry.key,
child: Text(entry.value),
);
},
),
).call,
),
],
),

View File

@ -2012,10 +2012,10 @@ packages:
dependency: "direct main"
description:
name: shadcn_flutter
sha256: a04b6ce51ff8486fe9c0c3b373605ab30b823507a79a4458fc74b056edc883d8
sha256: "1e5f40484a42217a69af254952168783d1305025d56dabc45ab16396dba84d5e"
url: "https://pub.dev"
source: hosted
version: "0.0.25"
version: "0.0.26"
shared_preferences:
dependency: "direct main"
description:

View File

@ -104,7 +104,7 @@ dependencies:
ref: dart-3-support
url: https://github.com/KRTirtho/scrobblenaut.git
scroll_to_index: ^3.0.1
shadcn_flutter: ^0.0.25
shadcn_flutter: ^0.0.26
shared_preferences: ^2.2.3
shelf: ^1.4.1
shelf_router: ^1.1.4