mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class CardColorService {
|
||||
static const _key1 = 'card_color_primary';
|
||||
static const _key2 = 'card_color_secondary';
|
||||
|
||||
// defaults match existing gradient: Color(0xFF6B5DD3) and Color(0xFF2A2040)
|
||||
static const defaultPrimary = Color(0xFF6B5DD3);
|
||||
static const defaultSecondary = Color(0xFF2A2040);
|
||||
|
||||
static Future<(Color, Color)> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final c1 = prefs.getInt(_key1);
|
||||
final c2 = prefs.getInt(_key2);
|
||||
return (
|
||||
c1 != null ? Color(c1) : defaultPrimary,
|
||||
c2 != null ? Color(c2) : defaultSecondary,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> save(Color primary, Color secondary) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_key1, primary.value);
|
||||
await prefs.setInt(_key2, secondary.value);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../core/services/card_color_service.dart';
|
||||
import '../../shared/models/transaction.dart';
|
||||
import '../../shared/services/storage_service.dart';
|
||||
import '../settings/provider.dart';
|
||||
@@ -147,3 +149,35 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
||||
final recentTransactionsProvider = Provider<List<Transaction>>((ref) {
|
||||
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
||||
});
|
||||
|
||||
// Loaded card colors state
|
||||
class CardColors {
|
||||
final Color primary;
|
||||
final Color secondary;
|
||||
|
||||
const CardColors(this.primary, this.secondary);
|
||||
}
|
||||
|
||||
final cardColorsProvider = StateNotifierProvider<CardColorsNotifier, CardColors>((ref) {
|
||||
return CardColorsNotifier();
|
||||
});
|
||||
|
||||
class CardColorsNotifier extends StateNotifier<CardColors> {
|
||||
CardColorsNotifier()
|
||||
: super(const CardColors(
|
||||
CardColorService.defaultPrimary,
|
||||
CardColorService.defaultSecondary,
|
||||
)) {
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final (c1, c2) = await CardColorService.load();
|
||||
state = CardColors(c1, c2);
|
||||
}
|
||||
|
||||
Future<void> save(Color primary, Color secondary) async {
|
||||
state = CardColors(primary, secondary);
|
||||
await CardColorService.save(primary, secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:sensors_plus/sensors_plus.dart';
|
||||
import '../../core/constants.dart';
|
||||
import '../../core/services/card_color_service.dart';
|
||||
import '../../shared/models/transaction.dart';
|
||||
import '../../shared/utils/currency_utils.dart';
|
||||
import '../../shared/providers/amount_format_provider.dart';
|
||||
@@ -40,15 +43,40 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final _searchController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
final _searchFocusNode = FocusNode();
|
||||
bool _editingCard = false;
|
||||
bool _editingPrimary = true;
|
||||
Color _tempPrimary = CardColorService.defaultPrimary;
|
||||
Color _tempSecondary = CardColorService.defaultSecondary;
|
||||
double _cardBottomY = 300;
|
||||
|
||||
Border? _themeBorder(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1);
|
||||
}
|
||||
|
||||
void _onCardLongPress() {
|
||||
final colors = ref.read(cardColorsProvider);
|
||||
_tempPrimary = colors.primary;
|
||||
_tempSecondary = colors.secondary;
|
||||
|
||||
// Calculate actual card bottom: status bar + appbar + top padding + card height
|
||||
final statusBar = MediaQuery.of(context).padding.top;
|
||||
final appBarHeight = kToolbarHeight;
|
||||
final topPadding = 16.0;
|
||||
final cardHeight = 180.0;
|
||||
_cardBottomY = statusBar + appBarHeight + topPadding + cardHeight;
|
||||
|
||||
setState(() => _editingCard = true);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() async {
|
||||
final colors = ref.read(cardColorsProvider);
|
||||
_tempPrimary = colors.primary;
|
||||
_tempSecondary = colors.secondary;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -79,7 +107,10 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
final currencyInfo = ref.watch(currencyProvider);
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
return Stack(
|
||||
children: [
|
||||
// NORMAL SCAFFOLD — always rendered, card is real and animated
|
||||
Scaffold(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
@@ -128,7 +159,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_BalanceCard(balance: balance, currencyInfo: currencyInfo),
|
||||
_BalanceCard(
|
||||
balance: balance,
|
||||
currencyInfo: currencyInfo,
|
||||
onLongPress: _onCardLongPress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_SummaryRow(
|
||||
income: income,
|
||||
@@ -187,6 +222,194 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// EDIT OVERLAY — only when editing
|
||||
if (_editingCard) _buildEditOverlay(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditOverlay(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Calculate card position to EXCLUDE it from blur
|
||||
// Blur only the area BELOW the card
|
||||
Positioned.fill(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top portion — card area — NOT blurred, transparent
|
||||
SizedBox(height: _cardBottomY),
|
||||
// Bottom portion — blurred
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _editingCard = false),
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.55),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Color editor panel — positioned below the card
|
||||
Positioned(
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: _cardBottomY + 16,
|
||||
child: GestureDetector(
|
||||
onTap: () {}, // prevent dismiss
|
||||
child: _buildColorPanel(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorPanel(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(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: [
|
||||
// Toggle between primary/secondary
|
||||
Row(
|
||||
children: [
|
||||
_PanelTab(
|
||||
label: 'Primary',
|
||||
isSelected: _editingPrimary,
|
||||
color: _tempPrimary,
|
||||
onTap: () => setState(() => _editingPrimary = true),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_PanelTab(
|
||||
label: 'Secondary',
|
||||
isSelected: !_editingPrimary,
|
||||
color: _tempSecondary,
|
||||
onTap: () => setState(() => _editingPrimary = false),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// HSV Color Picker
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: ColorPicker(
|
||||
pickerColor: _editingPrimary ? _tempPrimary : _tempSecondary,
|
||||
onColorChanged: (color) {
|
||||
setState(() {
|
||||
if (_editingPrimary) {
|
||||
_tempPrimary = color;
|
||||
} else {
|
||||
_tempSecondary = color;
|
||||
}
|
||||
});
|
||||
},
|
||||
colorPickerWidth: MediaQuery.of(context).size.width - 80,
|
||||
pickerAreaHeightPercent: 0.7,
|
||||
enableAlpha: false,
|
||||
displayThumbColor: true,
|
||||
labelTypes: const [],
|
||||
pickerAreaBorderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Confirm 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PanelTab extends StatelessWidget {
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final Color color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PanelTab({
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color.withOpacity(0.15) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isSelected ? color : Colors.white24,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white30, width: 1),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected ? color : Colors.white60,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -480,7 +703,13 @@ class _BudgetProgress extends ConsumerWidget {
|
||||
class _BalanceCard extends ConsumerStatefulWidget {
|
||||
final double balance;
|
||||
final CurrencyInfo currencyInfo;
|
||||
const _BalanceCard({required this.balance, required this.currencyInfo});
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
const _BalanceCard({
|
||||
required this.balance,
|
||||
required this.currencyInfo,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_BalanceCard> createState() => _BalanceCardState();
|
||||
@@ -521,6 +750,7 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
||||
Widget build(BuildContext context) {
|
||||
final rates = ref.read(exchangeRateServiceProvider);
|
||||
final fmt = ref.watch(amountFormatProvider);
|
||||
final colors = ref.watch(cardColorsProvider);
|
||||
final allCurrencies = [
|
||||
('USD', r'$'),
|
||||
('EUR', '€'),
|
||||
@@ -531,45 +761,13 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
||||
.where((c) => c.$1 != widget.currencyInfo.code)
|
||||
.toList();
|
||||
|
||||
return AnimatedBuilder(
|
||||
return GestureDetector(
|
||||
onLongPress: widget.onLongPress,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
_tiltX += (_targetTiltX - _tiltX) * 0.15;
|
||||
_tiltY += (_targetTiltY - _tiltY) * 0.15;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
final tiltBrightness = (_tiltX * 0.15 + _tiltY * 0.1).clamp(
|
||||
-0.15,
|
||||
0.15,
|
||||
);
|
||||
final highlightShift = (tiltBrightness * 40).round();
|
||||
|
||||
final topColor = isDark
|
||||
? Color.fromARGB(
|
||||
255,
|
||||
(0x7C + highlightShift).clamp(0x60, 0xFF),
|
||||
(0x6D + highlightShift).clamp(0x55, 0xFF),
|
||||
0xED,
|
||||
)
|
||||
: Color.fromARGB(
|
||||
255,
|
||||
(0x2A + highlightShift).clamp(0x20, 0x40),
|
||||
(0x25 + highlightShift).clamp(0x1A, 0x35),
|
||||
(0x45 + highlightShift).clamp(0x35, 0x55),
|
||||
);
|
||||
final bottomColor = isDark
|
||||
? Color.fromARGB(
|
||||
255,
|
||||
(0x2A - highlightShift).clamp(0x18, 0x40),
|
||||
(0x20 - highlightShift).clamp(0x14, 0x30),
|
||||
(0x60 - highlightShift).clamp(0x45, 0x75),
|
||||
)
|
||||
: Color.fromARGB(
|
||||
255,
|
||||
(0x14 - highlightShift).clamp(0x0A, 0x20),
|
||||
(0x12 - highlightShift).clamp(0x08, 0x1E),
|
||||
(0x28 - highlightShift).clamp(0x1E, 0x34),
|
||||
);
|
||||
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
@@ -583,12 +781,12 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
gradient: LinearGradient(
|
||||
begin: const Alignment(-1.0, -1.0),
|
||||
end: const Alignment(1.0, 1.0),
|
||||
begin: const Alignment(-0.5, -0.5),
|
||||
end: const Alignment(0.5, 0.5),
|
||||
colors: [
|
||||
topColor,
|
||||
isDark ? const Color(0xFF4A3FA0) : const Color(0xFF1A1530),
|
||||
bottomColor,
|
||||
colors.primary,
|
||||
colors.secondary,
|
||||
Color.lerp(colors.secondary, Colors.black, 0.3)!,
|
||||
],
|
||||
stops: const [0.0, 0.5, 1.0],
|
||||
),
|
||||
@@ -602,7 +800,10 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
child: Stack(
|
||||
children: [
|
||||
// existing card content
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
@@ -688,10 +889,28 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
||||
],
|
||||
),
|
||||
),
|
||||
// hint text — absolute position, bottom center, no layout impact
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Text(
|
||||
'tap and hold to edit',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white.withOpacity(0.18),
|
||||
letterSpacing: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies:
|
||||
http: ^1.2.0
|
||||
sensors_plus: ^6.1.0
|
||||
local_auth: ^2.3.0
|
||||
flutter_colorpicker: ^1.1.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
||||
Reference in New Issue
Block a user