This commit is contained in:
2026-03-21 01:50:12 +03:00
parent a6e52ca089
commit 8383661cc7
+344 -288
View File
@@ -53,6 +53,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
Color _savedSecondary = CardColorService.defaultSecondary; Color _savedSecondary = CardColorService.defaultSecondary;
HSVColor _savedPrimaryHSV = HSVColor.fromColor(CardColorService.defaultPrimary); HSVColor _savedPrimaryHSV = HSVColor.fromColor(CardColorService.defaultPrimary);
HSVColor _savedSecondaryHSV = HSVColor.fromColor(CardColorService.defaultSecondary); HSVColor _savedSecondaryHSV = HSVColor.fromColor(CardColorService.defaultSecondary);
OverlayEntry? _overlayEntry;
HSVColor get _currentHSV => _editingPrimary ? _tempPrimaryHSV : _tempSecondaryHSV; HSVColor get _currentHSV => _editingPrimary ? _tempPrimaryHSV : _tempSecondaryHSV;
@@ -75,18 +76,44 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
void _onCardLongPress() { void _onCardLongPress() {
final colors = ref.read(cardColorsProvider); final colors = ref.read(cardColorsProvider);
// save originals for cancel
_savedPrimary = colors.primary; _savedPrimary = colors.primary;
_savedSecondary = colors.secondary; _savedSecondary = colors.secondary;
_savedPrimaryHSV = HSVColor.fromColor(colors.primary); _savedPrimaryHSV = HSVColor.fromColor(colors.primary);
_savedSecondaryHSV = HSVColor.fromColor(colors.secondary); _savedSecondaryHSV = HSVColor.fromColor(colors.secondary);
// init temp
_tempPrimary = colors.primary; _tempPrimary = colors.primary;
_tempSecondary = colors.secondary; _tempSecondary = colors.secondary;
_tempPrimaryHSV = HSVColor.fromColor(colors.primary); _tempPrimaryHSV = HSVColor.fromColor(colors.primary);
_tempSecondaryHSV = HSVColor.fromColor(colors.secondary); _tempSecondaryHSV = HSVColor.fromColor(colors.secondary);
setState(() => _editingCard = true); setState(() {
_editingCard = true;
_editingPrimary = true;
});
_showOverlay();
}
void _showOverlay() {
_overlayEntry = OverlayEntry(
builder: (overlayContext) => _FullScreenBlurOverlay(
dashboardState: this,
context: context,
),
);
Overlay.of(context, rootOverlay: true).insert(_overlayEntry!);
}
void _closeOverlay({required bool apply}) {
if (apply) {
ref.read(cardColorsProvider.notifier).save(_tempPrimary, _tempSecondary);
} else {
setState(() {
_tempPrimary = _savedPrimary;
_tempSecondary = _savedSecondary;
});
}
_overlayEntry?.remove();
_overlayEntry = null;
setState(() => _editingCard = false);
} }
@override @override
@@ -101,6 +128,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
@override @override
void dispose() { void dispose() {
_overlayEntry?.remove();
_searchController.dispose(); _searchController.dispose();
_scrollController.dispose(); _scrollController.dispose();
_searchFocusNode.dispose(); _searchFocusNode.dispose();
@@ -127,10 +155,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
final currencyInfo = ref.watch(currencyProvider); final currencyInfo = ref.watch(currencyProvider);
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
return Stack( return Scaffold(
children: [
// NORMAL SCAFFOLD — always rendered, card is real and animated
Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
@@ -244,287 +269,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
], ],
), ),
), ),
),
// EDIT OVERLAY — only when editing
if (_editingCard) _buildEditOverlay(context),
],
);
}
Widget _buildEditOverlay(BuildContext context) {
final balance = ref.read(totalBalanceProvider);
final currencyInfo = ref.read(currencyProvider);
final cardTop = MediaQuery.of(context).padding.top + kToolbarHeight + 16;
final panelTop = cardTop + 180 + 16;
return Stack(
children: [
// FULL SCREEN BLUR — covers everything: appbar, card bg, footer
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
color: Colors.black.withOpacity(0.6),
),
),
),
// CARD HOLE — re-render card on top of blur so it appears unblurred
// Position it exactly where the real card is
Positioned(
top: cardTop,
left: 20,
right: 20,
child: IgnorePointer(
ignoring: false,
child: _BalanceCard(
balance: balance,
currencyInfo: currencyInfo,
onLongPress: null, // no re-trigger during edit
previewPrimary: _tempPrimary,
previewSecondary: _tempSecondary,
),
),
),
// DISMISS tap — only on the blurred area outside panel and card
Positioned.fill(
child: GestureDetector(
onTap: () {
setState(() {
_tempPrimary = _savedPrimary;
_tempSecondary = _savedSecondary;
_editingCard = false;
});
},
behavior: HitTestBehavior.translucent,
child: const SizedBox.expand(),
),
),
// COLOR EDITOR PANEL — above blur, below card
Positioned(
left: 20,
right: 20,
top: panelTop,
bottom: MediaQuery.of(context).padding.bottom + 16,
child: GestureDetector(
onTap: () {}, // prevent dismiss
behavior: HitTestBehavior.opaque,
child: _buildColorPanel(context),
),
),
],
);
}
Widget _buildColorPanel(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: double.infinity,
),
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Container(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP ROW: tabs + close button
Row(
children: [
_PanelTab(
label: 'Primary',
isSelected: _editingPrimary,
color: _tempPrimary,
onTap: () => setState(() => _editingPrimary = true),
),
const SizedBox(width: 10),
_PanelTab(
label: 'Secondary',
isSelected: !_editingPrimary,
color: _tempSecondary,
onTap: () => setState(() => _editingPrimary = false),
),
const Spacer(),
// CLOSE BUTTON
GestureDetector(
onTap: () {
setState(() {
_tempPrimary = _savedPrimary;
_tempSecondary = _savedSecondary;
_editingCard = false;
});
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.08),
shape: BoxShape.circle,
),
child: Icon(
Icons.close_rounded,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
),
],
),
const SizedBox(height: 16),
// 2D spectrum area — drag to pick saturation + value
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 180,
width: double.infinity,
child: ColorPickerArea(
_currentHSV,
_onHSVChanged,
PaletteType.hsvWithHue,
),
),
),
const SizedBox(height: 12),
// Hue rainbow slider — drag to change hue
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
height: 24,
child: ColorPickerSlider(
TrackType.hue,
_currentHSV,
_onHSVChanged,
displayThumbColor: true,
),
),
),
const SizedBox(height: 12),
// Color preview row
Row(
children: [
// PRIMARY — left aligned
GestureDetector(
onTap: () => setState(() => _editingPrimary = true),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: _tempPrimary,
borderRadius: BorderRadius.circular(8),
border: _editingPrimary
? Border.all(color: Colors.white, width: 2)
: Border.all(color: Colors.transparent, width: 2),
boxShadow: _editingPrimary
? [
BoxShadow(
color: _tempPrimary.withOpacity(0.5),
blurRadius: 8,
),
]
: null,
),
),
const SizedBox(width: 8),
Text(
'#${_tempPrimary.value.toRadixString(16).substring(2).toUpperCase()}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _editingPrimary
? Theme.of(context).colorScheme.onSurface.withOpacity(0.8)
: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
fontWeight: _editingPrimary ? FontWeight.w600 : FontWeight.normal,
fontFamily: 'monospace',
fontSize: 12,
),
),
],
),
),
const Spacer(),
// SECONDARY — right aligned
GestureDetector(
onTap: () => setState(() => _editingPrimary = false),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'#${_tempSecondary.value.toRadixString(16).substring(2).toUpperCase()}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: !_editingPrimary
? Theme.of(context).colorScheme.onSurface.withOpacity(0.8)
: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
fontWeight: !_editingPrimary ? FontWeight.w600 : FontWeight.normal,
fontFamily: 'monospace',
fontSize: 12,
),
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: _tempSecondary,
borderRadius: BorderRadius.circular(8),
border: !_editingPrimary
? Border.all(color: Colors.white, width: 2)
: Border.all(color: Colors.transparent, width: 2),
boxShadow: !_editingPrimary
? [
BoxShadow(
color: _tempSecondary.withOpacity(0.5),
blurRadius: 8,
),
]
: null,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// APPLY BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
ref.read(cardColorsProvider.notifier).save(_tempPrimary, _tempSecondary);
setState(() => _editingCard = false);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF7C6DED),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Apply',
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
),
],
),
),
),
); );
} }
} }
@@ -1336,3 +1080,315 @@ class _EmptyState extends StatelessWidget {
); );
} }
} }
class _FullScreenBlurOverlay extends StatefulWidget {
final _DashboardScreenState dashboardState;
final BuildContext context;
const _FullScreenBlurOverlay({
required this.dashboardState,
required this.context,
});
@override
State<_FullScreenBlurOverlay> createState() => _FullScreenBlurOverlayState();
}
class _FullScreenBlurOverlayState extends State<_FullScreenBlurOverlay> {
_DashboardScreenState get dash => widget.dashboardState;
@override
Widget build(BuildContext context) {
final mq = MediaQuery.of(widget.context);
final cardTop = mq.padding.top + kToolbarHeight + 16;
final cardHeight = 180.0;
final panelTop = cardTop + cardHeight + 16;
final panelBottom = mq.padding.bottom + 16;
return Material(
color: Colors.transparent,
child: Stack(
children: [
// FULL SCREEN BLUR — covers navigator, appbar, footer, everything
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
color: Colors.black.withOpacity(0.6),
),
),
),
// DISMISS tap area
Positioned.fill(
child: GestureDetector(
onTap: () {
dash._closeOverlay(apply: false);
},
behavior: HitTestBehavior.translucent,
child: const SizedBox.expand(),
),
),
// CARD re-rendered sharp on top of blur, with live preview colors
Positioned(
top: cardTop,
left: 20,
right: 20,
child: Consumer(
builder: (ctx, ref, _) => _BalanceCard(
balance: ref.read(totalBalanceProvider),
currencyInfo: ref.read(currencyProvider),
onLongPress: null,
previewPrimary: dash._tempPrimary,
previewSecondary: dash._tempSecondary,
),
),
),
// COLOR PANEL
Positioned(
top: panelTop,
left: 20,
right: 20,
bottom: panelBottom,
child: GestureDetector(
onTap: () {},
behavior: HitTestBehavior.opaque,
child: _buildPanel(context, mq),
),
),
],
),
);
}
Widget _buildPanel(BuildContext context, MediaQueryData mq) {
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Container(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
decoration: BoxDecoration(
color: Theme.of(widget.context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: StatefulBuilder(
builder: (ctx, setPanelState) {
final isDark = Theme.of(widget.context).brightness == Brightness.dark;
void onHSVChanged(HSVColor hsv) {
setPanelState(() {});
dash.setState(() {
if (dash._editingPrimary) {
dash._tempPrimaryHSV = hsv;
dash._tempPrimary = hsv.toColor();
} else {
dash._tempSecondaryHSV = hsv;
dash._tempSecondary = hsv.toColor();
}
});
}
final currentHSV = dash._editingPrimary
? dash._tempPrimaryHSV
: dash._tempSecondaryHSV;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// TOP ROW: tabs + X
Row(
children: [
_PanelTab(
label: 'Primary',
isSelected: dash._editingPrimary,
color: dash._tempPrimary,
onTap: () {
dash.setState(() => dash._editingPrimary = true);
setPanelState(() {});
},
),
const SizedBox(width: 10),
_PanelTab(
label: 'Secondary',
isSelected: !dash._editingPrimary,
color: dash._tempSecondary,
onTap: () {
dash.setState(() => dash._editingPrimary = false);
setPanelState(() {});
},
),
const Spacer(),
GestureDetector(
onTap: () => dash._closeOverlay(apply: false),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Theme.of(widget.context)
.colorScheme
.onSurface
.withOpacity(0.08),
shape: BoxShape.circle,
),
child: Icon(
Icons.close_rounded,
size: 18,
color: Theme.of(widget.context)
.colorScheme
.onSurface
.withOpacity(0.6),
),
),
),
],
),
const SizedBox(height: 16),
// 2D SPECTRUM AREA
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
height: 160,
width: double.infinity,
child: ColorPickerArea(
currentHSV,
onHSVChanged,
PaletteType.hsvWithHue,
),
),
),
const SizedBox(height: 12),
// HUE SLIDER
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(
height: 22,
child: ColorPickerSlider(
TrackType.hue,
currentHSV,
onHSVChanged,
displayThumbColor: true,
),
),
),
const SizedBox(height: 12),
// COLOR PREVIEW ROW
Row(
children: [
GestureDetector(
onTap: () {
dash.setState(() => dash._editingPrimary = true);
setPanelState(() {});
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: dash._tempPrimary,
borderRadius: BorderRadius.circular(8),
border: dash._editingPrimary
? Border.all(color: Colors.white, width: 2)
: Border.all(
color: Colors.transparent, width: 2),
),
),
const SizedBox(width: 8),
Text(
'#${dash._tempPrimary.value.toRadixString(16).substring(2).toUpperCase()}',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
fontWeight: dash._editingPrimary
? FontWeight.w600
: FontWeight.normal,
color: Theme.of(widget.context)
.colorScheme
.onSurface
.withOpacity(
dash._editingPrimary ? 0.8 : 0.4,
),
),
),
],
),
),
const Spacer(),
GestureDetector(
onTap: () {
dash.setState(() => dash._editingPrimary = false);
setPanelState(() {});
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'#${dash._tempSecondary.value.toRadixString(16).substring(2).toUpperCase()}',
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
fontWeight: !dash._editingPrimary
? FontWeight.w600
: FontWeight.normal,
color: Theme.of(widget.context)
.colorScheme
.onSurface
.withOpacity(
!dash._editingPrimary ? 0.8 : 0.4,
),
),
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: dash._tempSecondary,
borderRadius: BorderRadius.circular(8),
border: !dash._editingPrimary
? Border.all(color: Colors.white, width: 2)
: Border.all(
color: Colors.transparent, width: 2),
),
),
],
),
),
],
),
const SizedBox(height: 16),
// APPLY BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => dash._closeOverlay(apply: true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF7C6DED),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
child: const Text(
'Apply',
style:
TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
),
),
),
],
);
},
),
),
);
}
}