1+ import 'dart:async' ;
2+
13import 'package:flutter/material.dart' as material;
24import 'package:querya_desktop/core/layout/ui_scale.dart' ;
5+ import 'package:querya_desktop/core/theme/querya_theme.dart' ;
36import 'package:querya_desktop/core/theme/theme_definition.dart' ;
7+ import 'package:querya_desktop/features/settings/theme_preview_card.dart' ;
48import 'package:querya_desktop/shared/widgets/querya_dropdown_tokens.dart' ;
59import 'package:shadcn_flutter/shadcn_flutter.dart' ;
610
11+ /// Debounce delay before requesting a hover preview load.
12+ const Duration themePreviewDebounce = Duration (milliseconds: 120 );
13+
714/// Dedicated theme picker for large registry lists (50+ themes).
815class ThemePickerButton extends material.StatefulWidget {
916 const ThemePickerButton ({
1017 super .key,
1118 required this .themes,
1219 required this .selectedThemeId,
1320 required this .onSelected,
21+ this .onPreviewTheme,
1422 this .isLoading = false ,
1523 this .expandToParent = false ,
1624 this .width,
@@ -19,6 +27,10 @@ class ThemePickerButton extends material.StatefulWidget {
1927 final List <ThemeDefinition > themes;
2028 final String ? selectedThemeId;
2129 final material.ValueChanged <String > onSelected;
30+
31+ /// Loads preview data for [themeId] on hover. Must not apply the theme.
32+ final Future <ThemePreviewResult > Function (String themeId)? onPreviewTheme;
33+
2234 final bool isLoading;
2335 final bool expandToParent;
2436 final double ? width;
@@ -54,6 +66,13 @@ class _ThemePickerButtonState extends material.State<ThemePickerButton> {
5466 final material.TextEditingController _searchController =
5567 material.TextEditingController ();
5668 bool _triggerHovered = false ;
69+ String ? _previewThemeId;
70+ String ? _previewThemeLabel;
71+ QueryaTheme ? _previewTheme;
72+ String ? _previewError;
73+ bool _previewLoading = false ;
74+ Timer ? _previewDebounce;
75+ int _previewRequestSerial = 0 ;
5776
5877 @override
5978 void initState () {
@@ -63,12 +82,67 @@ class _ThemePickerButtonState extends material.State<ThemePickerButton> {
6382
6483 @override
6584 void dispose () {
85+ _previewDebounce? .cancel ();
6686 _searchController.removeListener (_onSearchChanged);
6787 _searchController.dispose ();
6888 _scrollController.dispose ();
6989 super .dispose ();
7090 }
7191
92+ void _resetPreviewState () {
93+ _previewDebounce? .cancel ();
94+ _previewRequestSerial++ ;
95+ _previewThemeId = null ;
96+ _previewThemeLabel = null ;
97+ _previewTheme = null ;
98+ _previewError = null ;
99+ _previewLoading = false ;
100+ }
101+
102+ void _schedulePreview (ThemeDefinition definition) {
103+ if (widget.onPreviewTheme == null ) return ;
104+
105+ _previewDebounce? .cancel ();
106+ final targetId = definition.id;
107+ setState (() {
108+ _previewThemeId = targetId;
109+ _previewThemeLabel = definition.name;
110+ });
111+
112+ _previewDebounce = Timer (themePreviewDebounce, () {
113+ if (! mounted || _previewThemeId != targetId) return ;
114+ setState (() {
115+ _previewLoading = true ;
116+ _previewTheme = null ;
117+ _previewError = null ;
118+ });
119+ unawaited (_loadPreview (targetId));
120+ });
121+ }
122+
123+ Future <void > _loadPreview (String themeId) async {
124+ final loader = widget.onPreviewTheme;
125+ if (loader == null || ! mounted || _previewThemeId != themeId) return ;
126+
127+ final requestId = ++ _previewRequestSerial;
128+ final result = await loader (themeId);
129+ if (! mounted || requestId != _previewRequestSerial) return ;
130+
131+ setState (() {
132+ _previewLoading = false ;
133+ switch (result) {
134+ case ThemePreviewSuccess (: final theme):
135+ _previewTheme = theme;
136+ _previewError = null ;
137+ case ThemePreviewFailure (: final message):
138+ _previewTheme = null ;
139+ _previewError = message;
140+ case ThemePreviewLoading ():
141+ _previewLoading = true ;
142+ }
143+ });
144+ }
145+
72146 void _onSearchChanged () {
73147 setState (() {});
74148 if (_scrollController.hasClients) {
@@ -216,6 +290,21 @@ class _ThemePickerButtonState extends material.State<ThemePickerButton> {
216290 ),
217291 ),
218292 ),
293+ if (widget.onPreviewTheme != null )
294+ material.Padding (
295+ padding: material.EdgeInsets .fromLTRB (
296+ context.scaled (8 ),
297+ context.scaled (4 ),
298+ context.scaled (8 ),
299+ context.scaled (4 ),
300+ ),
301+ child: ThemePreviewCard (
302+ theme: _previewTheme,
303+ errorMessage: _previewError,
304+ isLoading: _previewLoading,
305+ label: _previewThemeLabel,
306+ ),
307+ ),
219308 material.Expanded (
220309 child: filteredThemes.isEmpty
221310 ? material.Center (
@@ -245,9 +334,13 @@ class _ThemePickerButtonState extends material.State<ThemePickerButton> {
245334 definition: theme,
246335 selected: theme.id == widget.selectedThemeId,
247336 colorScheme: cs,
337+ onHover: widget.onPreviewTheme == null
338+ ? null
339+ : () => _schedulePreview (theme),
248340 onSelected: () {
249341 widget.onSelected (theme.id);
250342 _clearSearch ();
343+ _resetPreviewState ();
251344 _controller.close ();
252345 },
253346 );
@@ -332,6 +425,7 @@ class _ThemePickerButtonState extends material.State<ThemePickerButton> {
332425 controller.close ();
333426 } else {
334427 _clearSearch ();
428+ _resetPreviewState ();
335429 controller.open ();
336430 }
337431 }
@@ -349,12 +443,14 @@ class _ThemePickerRow extends material.StatefulWidget {
349443 required this .selected,
350444 required this .colorScheme,
351445 required this .onSelected,
446+ this .onHover,
352447 });
353448
354449 final ThemeDefinition definition;
355450 final bool selected;
356451 final ColorScheme colorScheme;
357452 final material.VoidCallback onSelected;
453+ final material.VoidCallback ? onHover;
358454
359455 @override
360456 material.State <_ThemePickerRow > createState () => _ThemePickerRowState ();
@@ -377,7 +473,10 @@ class _ThemePickerRowState extends material.State<_ThemePickerRow> {
377473
378474 return material.MouseRegion (
379475 cursor: material.SystemMouseCursors .click,
380- onEnter: (_) => setState (() => _hovered = true ),
476+ onEnter: (_) {
477+ setState (() => _hovered = true );
478+ widget.onHover? .call ();
479+ },
381480 onExit: (_) => setState (() => _hovered = false ),
382481 child: material.Material (
383482 type: material.MaterialType .transparency,
0 commit comments