V1 - Personal Account and Budgeting Done

Personal Account and Budgeting
This commit is contained in:
2026-08-16 20:13:21 +05:30
commit 17c0526ed6
247 changed files with 19002 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
class AttachmentGalleryScreen extends StatefulWidget {
final List<ImageProvider> images;
final int initialIndex;
const AttachmentGalleryScreen({
super.key,
required this.images,
required this.initialIndex,
});
@override
State<AttachmentGalleryScreen> createState() => _AttachmentGalleryScreenState();
}
class _AttachmentGalleryScreenState extends State<AttachmentGalleryScreen> {
late PageController _pageController;
int _currentIndex = 0;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
title: Text(
'${_currentIndex + 1} / ${widget.images.length}',
style: const TextStyle(color: Colors.white),
),
),
body: PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.images.length,
itemBuilder: (context, index) {
return _ZoomableImage(image: widget.images[index]);
},
),
);
}
}
class _ZoomableImage extends StatefulWidget {
final ImageProvider image;
const _ZoomableImage({required this.image});
@override
State<_ZoomableImage> createState() => _ZoomableImageState();
}
class _ZoomableImageState extends State<_ZoomableImage> with SingleTickerProviderStateMixin {
final TransformationController _transformationController = TransformationController();
late AnimationController _animationController;
Animation<Matrix4>? _animation;
TapDownDetails? _doubleTapDetails;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
)..addListener(() {
if (_animation != null) {
_transformationController.value = _animation!.value;
}
});
}
@override
void dispose() {
_animationController.dispose();
_transformationController.dispose();
super.dispose();
}
void _handleDoubleTap() {
if (_doubleTapDetails == null) return;
final position = _doubleTapDetails!.localPosition;
final matrix = _transformationController.value;
final scale = matrix.getMaxScaleOnAxis();
Matrix4 endMatrix;
if (scale > 1.0) {
// Zoom out
endMatrix = Matrix4.identity();
} else {
// Zoom in
endMatrix = Matrix4.identity()
..translate(-position.dx * 1.5, -position.dy * 1.5)
..scale(2.5);
}
_animation = Matrix4Tween(
begin: _transformationController.value,
end: endMatrix,
).animate(CurveTween(curve: Curves.easeInOut).animate(_animationController));
_animationController.forward(from: 0);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onDoubleTapDown: (d) => _doubleTapDetails = d,
onDoubleTap: _handleDoubleTap,
child: InteractiveViewer(
transformationController: _transformationController,
minScale: 1.0,
maxScale: 4.0,
child: Image(
image: widget.image,
fit: BoxFit.contain,
),
),
);
}
}

View File

@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../providers/providers.dart';
import '../../providers/paginated_transaction_provider.dart';
class TransactionFilterSheet extends ConsumerStatefulWidget {
const TransactionFilterSheet({super.key});
@override
ConsumerState<TransactionFilterSheet> createState() => _TransactionFilterSheetState();
}
class _TransactionFilterSheetState extends ConsumerState<TransactionFilterSheet> {
String? _selectedType;
int? _selectedWalletId;
int? _selectedCategoryId;
@override
void initState() {
super.initState();
final currentState = ref.read(paginatedTransactionProvider);
_selectedType = currentState.type;
_selectedWalletId = currentState.walletId;
_selectedCategoryId = currentState.categoryId;
}
void _applyFilters() {
ref.read(paginatedTransactionProvider.notifier).updateFilters(
type: _selectedType,
walletId: _selectedWalletId,
categoryId: _selectedCategoryId,
);
Navigator.pop(context);
}
void _resetFilters() {
ref.read(paginatedTransactionProvider.notifier).clearFilters();
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
final walletsState = ref.watch(walletProvider);
final categoriesState = ref.watch(categoryProvider);
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Filter Transactions', style: Theme.of(context).textTheme.titleLarge),
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.pop(context),
),
],
),
const SizedBox(height: 24),
// Type Filter
Text('Transaction Type', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'ALL', label: Text('All')),
ButtonSegment(value: 'EXPENSE', label: Text('Expense')),
ButtonSegment(value: 'INCOME', label: Text('Income')),
],
selected: {_selectedType ?? 'ALL'},
onSelectionChanged: (set) {
setState(() => _selectedType = set.first == 'ALL' ? null : set.first);
},
),
const SizedBox(height: 24),
// Wallet Filter
Text('Wallet', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: _selectedWalletId,
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87, fontSize: 16),
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
items: [
const DropdownMenuItem(value: null, child: Text('All Wallets')),
if (walletsState.hasValue)
...walletsState.value!.map((w) => DropdownMenuItem(
value: w.id,
child: Text(w.name),
)),
],
onChanged: (val) => setState(() => _selectedWalletId = val),
),
const SizedBox(height: 24),
// Category Filter
Text('Category', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
DropdownButtonFormField<int>(
value: _selectedCategoryId,
style: const TextStyle(fontWeight: FontWeight.w500, color: Colors.black87, fontSize: 16),
decoration: InputDecoration(
filled: true,
fillColor: Colors.grey.shade100,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xFF6C63FF), width: 2)),
),
items: [
const DropdownMenuItem(value: null, child: Text('All Categories')),
if (categoriesState.hasValue)
...categoriesState.value!.map((c) => DropdownMenuItem(
value: c.id,
child: Text(c.name),
)),
],
onChanged: (val) => setState(() => _selectedCategoryId = val),
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _resetFilters,
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
foregroundColor: Colors.grey.shade700,
side: BorderSide(color: Colors.grey.shade300),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: const Text('Reset', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton(
onPressed: _applyFilters,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: const Color(0xFF6C63FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
),
child: const Text('Apply Filters', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
],
),
),
),
);
}
}