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:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../../core/services/card_color_service.dart';
|
||||||
import '../../shared/models/transaction.dart';
|
import '../../shared/models/transaction.dart';
|
||||||
import '../../shared/services/storage_service.dart';
|
import '../../shared/services/storage_service.dart';
|
||||||
import '../settings/provider.dart';
|
import '../settings/provider.dart';
|
||||||
@@ -147,3 +149,35 @@ final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
|
|||||||
final recentTransactionsProvider = Provider<List<Transaction>>((ref) {
|
final recentTransactionsProvider = Provider<List<Transaction>>((ref) {
|
||||||
return ref.watch(filteredTransactionsProvider).take(20).toList();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+456
-237
@@ -1,11 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:sensors_plus/sensors_plus.dart';
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
|
import '../../core/services/card_color_service.dart';
|
||||||
import '../../shared/models/transaction.dart';
|
import '../../shared/models/transaction.dart';
|
||||||
import '../../shared/utils/currency_utils.dart';
|
import '../../shared/utils/currency_utils.dart';
|
||||||
import '../../shared/providers/amount_format_provider.dart';
|
import '../../shared/providers/amount_format_provider.dart';
|
||||||
@@ -40,15 +43,40 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
final _searchFocusNode = FocusNode();
|
final _searchFocusNode = FocusNode();
|
||||||
|
bool _editingCard = false;
|
||||||
|
bool _editingPrimary = true;
|
||||||
|
Color _tempPrimary = CardColorService.defaultPrimary;
|
||||||
|
Color _tempSecondary = CardColorService.defaultSecondary;
|
||||||
|
double _cardBottomY = 300;
|
||||||
|
|
||||||
Border? _themeBorder(BuildContext context) {
|
Border? _themeBorder(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
return isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1);
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
Future.microtask(() async {
|
||||||
|
final colors = ref.read(cardColorsProvider);
|
||||||
|
_tempPrimary = colors.primary;
|
||||||
|
_tempSecondary = colors.secondary;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -79,111 +107,306 @@ 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 Scaffold(
|
return Stack(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
children: [
|
||||||
appBar: AppBar(
|
// NORMAL SCAFFOLD — always rendered, card is real and animated
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
Scaffold(
|
||||||
elevation: 0,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
scrolledUnderElevation: 0,
|
appBar: AppBar(
|
||||||
titleSpacing: 20,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
title: Text(
|
elevation: 0,
|
||||||
'Casha',
|
scrolledUnderElevation: 0,
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
titleSpacing: 20,
|
||||||
fontWeight: FontWeight.w800,
|
title: Text(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
'Casha',
|
||||||
letterSpacing: -0.5,
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 20),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
DateFormat('MMMM yyyy').format(DateTime.now()),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () => context.push('/add'),
|
||||||
|
backgroundColor: const Color(0xFF7C6DED),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
|
body: SafeArea(
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
cacheExtent: 300,
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_BalanceCard(
|
||||||
|
balance: balance,
|
||||||
|
currencyInfo: currencyInfo,
|
||||||
|
onLongPress: _onCardLongPress,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_SummaryRow(
|
||||||
|
income: income,
|
||||||
|
expense: expense,
|
||||||
|
currencyInfo: currencyInfo,
|
||||||
|
),
|
||||||
|
if (budget != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_BudgetProgress(
|
||||||
|
spent: monthExpense,
|
||||||
|
budget: budget,
|
||||||
|
currencyInfo: currencyInfo,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_SearchBar(
|
||||||
|
controller: _searchController,
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
onTap: _scrollToSearch,
|
||||||
|
ref: ref,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const FilterChips(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Transactions',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (recent.isEmpty)
|
||||||
|
const SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: _EmptyState(),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 100),
|
||||||
|
sliver: SliverList.builder(
|
||||||
|
itemCount: recent.length,
|
||||||
|
itemBuilder: (context, i) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: _TransactionTile(transaction: recent[i]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: 80)),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
// EDIT OVERLAY — only when editing
|
||||||
Padding(
|
if (_editingCard) _buildEditOverlay(context),
|
||||||
padding: const EdgeInsets.only(right: 20),
|
],
|
||||||
child: Center(
|
);
|
||||||
child: Text(
|
}
|
||||||
DateFormat('MMMM yyyy').format(DateTime.now()),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
Widget _buildEditOverlay(BuildContext context) {
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
return Stack(
|
||||||
fontWeight: FontWeight.w500,
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
);
|
||||||
onPressed: () => context.push('/add'),
|
}
|
||||||
backgroundColor: const Color(0xFF7C6DED),
|
}
|
||||||
foregroundColor: Colors.white,
|
|
||||||
icon: const Icon(Icons.add),
|
class _PanelTab extends StatelessWidget {
|
||||||
label: const Text('Add', style: TextStyle(fontWeight: FontWeight.w600)),
|
final String label;
|
||||||
),
|
final bool isSelected;
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
final Color color;
|
||||||
body: SafeArea(
|
final VoidCallback onTap;
|
||||||
child: CustomScrollView(
|
|
||||||
controller: _scrollController,
|
const _PanelTab({
|
||||||
cacheExtent: 300,
|
required this.label,
|
||||||
slivers: [
|
required this.isSelected,
|
||||||
SliverToBoxAdapter(
|
required this.color,
|
||||||
child: Padding(
|
required this.onTap,
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
});
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
@override
|
||||||
children: [
|
Widget build(BuildContext context) {
|
||||||
_BalanceCard(balance: balance, currencyInfo: currencyInfo),
|
return GestureDetector(
|
||||||
const SizedBox(height: 16),
|
onTap: onTap,
|
||||||
_SummaryRow(
|
child: AnimatedContainer(
|
||||||
income: income,
|
duration: const Duration(milliseconds: 150),
|
||||||
expense: expense,
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
currencyInfo: currencyInfo,
|
decoration: BoxDecoration(
|
||||||
),
|
color: isSelected ? color.withOpacity(0.15) : Colors.transparent,
|
||||||
if (budget != null) ...[
|
borderRadius: BorderRadius.circular(10),
|
||||||
const SizedBox(height: 16),
|
border: Border.all(
|
||||||
_BudgetProgress(
|
color: isSelected ? color : Colors.white24,
|
||||||
spent: monthExpense,
|
width: 1.5,
|
||||||
budget: budget,
|
),
|
||||||
currencyInfo: currencyInfo,
|
),
|
||||||
),
|
child: Row(
|
||||||
],
|
mainAxisSize: MainAxisSize.min,
|
||||||
const SizedBox(height: 24),
|
children: [
|
||||||
_SearchBar(
|
Container(
|
||||||
controller: _searchController,
|
width: 14,
|
||||||
focusNode: _searchFocusNode,
|
height: 14,
|
||||||
onTap: _scrollToSearch,
|
decoration: BoxDecoration(
|
||||||
ref: ref,
|
color: color,
|
||||||
),
|
shape: BoxShape.circle,
|
||||||
const SizedBox(height: 12),
|
border: Border.all(color: Colors.white30, width: 1),
|
||||||
const FilterChips(),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'Transactions',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (recent.isEmpty)
|
const SizedBox(width: 8),
|
||||||
const SliverFillRemaining(
|
Text(
|
||||||
hasScrollBody: false,
|
label,
|
||||||
child: _EmptyState(),
|
style: TextStyle(
|
||||||
)
|
fontSize: 13,
|
||||||
else
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
SliverPadding(
|
color: isSelected ? color : Colors.white60,
|
||||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 100),
|
|
||||||
sliver: SliverList.builder(
|
|
||||||
itemCount: recent.length,
|
|
||||||
itemBuilder: (context, i) => Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 10),
|
|
||||||
child: RepaintBoundary(
|
|
||||||
child: _TransactionTile(transaction: recent[i]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: 80)),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -480,7 +703,13 @@ class _BudgetProgress extends ConsumerWidget {
|
|||||||
class _BalanceCard extends ConsumerStatefulWidget {
|
class _BalanceCard extends ConsumerStatefulWidget {
|
||||||
final double balance;
|
final double balance;
|
||||||
final CurrencyInfo currencyInfo;
|
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
|
@override
|
||||||
ConsumerState<_BalanceCard> createState() => _BalanceCardState();
|
ConsumerState<_BalanceCard> createState() => _BalanceCardState();
|
||||||
@@ -521,6 +750,7 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rates = ref.read(exchangeRateServiceProvider);
|
final rates = ref.read(exchangeRateServiceProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
|
final colors = ref.watch(cardColorsProvider);
|
||||||
final allCurrencies = [
|
final allCurrencies = [
|
||||||
('USD', r'$'),
|
('USD', r'$'),
|
||||||
('EUR', '€'),
|
('EUR', '€'),
|
||||||
@@ -531,167 +761,156 @@ class _BalanceCardState extends ConsumerState<_BalanceCard>
|
|||||||
.where((c) => c.$1 != widget.currencyInfo.code)
|
.where((c) => c.$1 != widget.currencyInfo.code)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return GestureDetector(
|
||||||
animation: _controller,
|
onLongPress: widget.onLongPress,
|
||||||
builder: (context, _) {
|
child: AnimatedBuilder(
|
||||||
_tiltX += (_targetTiltX - _tiltX) * 0.15;
|
animation: _controller,
|
||||||
_tiltY += (_targetTiltY - _tiltY) * 0.15;
|
builder: (context, _) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
_tiltX += (_targetTiltX - _tiltX) * 0.15;
|
||||||
|
_tiltY += (_targetTiltY - _tiltY) * 0.15;
|
||||||
|
|
||||||
final tiltBrightness = (_tiltX * 0.15 + _tiltY * 0.1).clamp(
|
return Transform(
|
||||||
-0.15,
|
alignment: Alignment.center,
|
||||||
0.15,
|
transform: Matrix4.identity()
|
||||||
);
|
..setEntry(3, 2, 0.001)
|
||||||
final highlightShift = (tiltBrightness * 40).round();
|
..rotateX(_tiltX * 0.42)
|
||||||
|
..rotateY(_tiltY * 0.42),
|
||||||
final topColor = isDark
|
child: Container(
|
||||||
? Color.fromARGB(
|
width: double.infinity,
|
||||||
255,
|
height: 180,
|
||||||
(0x7C + highlightShift).clamp(0x60, 0xFF),
|
decoration: BoxDecoration(
|
||||||
(0x6D + highlightShift).clamp(0x55, 0xFF),
|
borderRadius: BorderRadius.circular(20),
|
||||||
0xED,
|
gradient: LinearGradient(
|
||||||
)
|
begin: const Alignment(-0.5, -0.5),
|
||||||
: Color.fromARGB(
|
end: const Alignment(0.5, 0.5),
|
||||||
255,
|
colors: [
|
||||||
(0x2A + highlightShift).clamp(0x20, 0x40),
|
colors.primary,
|
||||||
(0x25 + highlightShift).clamp(0x1A, 0x35),
|
colors.secondary,
|
||||||
(0x45 + highlightShift).clamp(0x35, 0x55),
|
Color.lerp(colors.secondary, Colors.black, 0.3)!,
|
||||||
);
|
],
|
||||||
final bottomColor = isDark
|
stops: const [0.0, 0.5, 1.0],
|
||||||
? Color.fromARGB(
|
),
|
||||||
255,
|
boxShadow: [
|
||||||
(0x2A - highlightShift).clamp(0x18, 0x40),
|
BoxShadow(
|
||||||
(0x20 - highlightShift).clamp(0x14, 0x30),
|
color: Colors.black.withOpacity(0.4),
|
||||||
(0x60 - highlightShift).clamp(0x45, 0x75),
|
blurRadius: 20,
|
||||||
)
|
offset: const Offset(0, 8),
|
||||||
: Color.fromARGB(
|
),
|
||||||
255,
|
|
||||||
(0x14 - highlightShift).clamp(0x0A, 0x20),
|
|
||||||
(0x12 - highlightShift).clamp(0x08, 0x1E),
|
|
||||||
(0x28 - highlightShift).clamp(0x1E, 0x34),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Transform(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
transform: Matrix4.identity()
|
|
||||||
..setEntry(3, 2, 0.001)
|
|
||||||
..rotateX(_tiltX * 0.42)
|
|
||||||
..rotateY(_tiltY * 0.42),
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 180,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: const Alignment(-1.0, -1.0),
|
|
||||||
end: const Alignment(1.0, 1.0),
|
|
||||||
colors: [
|
|
||||||
topColor,
|
|
||||||
isDark ? const Color(0xFF4A3FA0) : const Color(0xFF1A1530),
|
|
||||||
bottomColor,
|
|
||||||
],
|
],
|
||||||
stops: const [0.0, 0.5, 1.0],
|
|
||||||
),
|
),
|
||||||
boxShadow: [
|
child: ClipRRect(
|
||||||
BoxShadow(
|
borderRadius: BorderRadius.circular(20),
|
||||||
color: Colors.black.withOpacity(0.4),
|
child: Stack(
|
||||||
blurRadius: 20,
|
|
||||||
offset: const Offset(0, 8),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
vertical: 20,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
// existing card content
|
||||||
flex: 5,
|
Padding(
|
||||||
child: Column(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 20,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
'TOTAL BALANCE',
|
flex: 5,
|
||||||
style: TextStyle(
|
child: Column(
|
||||||
fontSize: 11,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
letterSpacing: 1.5,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
color: Colors.white.withOpacity(0.6),
|
children: [
|
||||||
|
Text(
|
||||||
|
'TOTAL BALANCE',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
_smartBalance(
|
||||||
|
widget.balance,
|
||||||
|
fmt,
|
||||||
|
widget.currencyInfo.symbol,
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
if (widget.balance != 0) ...[
|
||||||
FittedBox(
|
const SizedBox(width: 16),
|
||||||
fit: BoxFit.scaleDown,
|
Container(
|
||||||
alignment: Alignment.center,
|
width: 1,
|
||||||
child: Text(
|
height: 70,
|
||||||
_smartBalance(
|
color: Colors.white.withOpacity(0.15),
|
||||||
widget.balance,
|
|
||||||
fmt,
|
|
||||||
widget.currencyInfo.symbol,
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 16),
|
||||||
|
SizedBox(
|
||||||
|
width: 110,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: others.map((c) {
|
||||||
|
final converted = rates.convert(
|
||||||
|
widget.balance,
|
||||||
|
widget.currencyInfo.code,
|
||||||
|
c.$1,
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
_smartBalance(converted, fmt, c.$2),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.white.withOpacity(0.65),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.balance != 0) ...[
|
// hint text — absolute position, bottom center, no layout impact
|
||||||
const SizedBox(width: 16),
|
Positioned(
|
||||||
Container(
|
bottom: 8,
|
||||||
width: 1,
|
left: 0,
|
||||||
height: 70,
|
right: 0,
|
||||||
color: Colors.white.withOpacity(0.15),
|
child: Text(
|
||||||
),
|
'tap and hold to edit',
|
||||||
const SizedBox(width: 16),
|
textAlign: TextAlign.center,
|
||||||
SizedBox(
|
style: TextStyle(
|
||||||
width: 110,
|
fontSize: 9,
|
||||||
child: Column(
|
color: Colors.white.withOpacity(0.18),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
letterSpacing: 0.6,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: others.map((c) {
|
|
||||||
final converted = rates.convert(
|
|
||||||
widget.balance,
|
|
||||||
widget.currencyInfo.code,
|
|
||||||
c.$1,
|
|
||||||
);
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
_smartBalance(converted, fmt, c.$2),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: Colors.white.withOpacity(0.65),
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies:
|
|||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
sensors_plus: ^6.1.0
|
sensors_plus: ^6.1.0
|
||||||
local_auth: ^2.3.0
|
local_auth: ^2.3.0
|
||||||
|
flutter_colorpicker: ^1.1.0
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
|||||||
Reference in New Issue
Block a user