V1 - Personal Account and Budgeting Done
Personal Account and Budgeting
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user