Skip to content

Commit 28453f8

Browse files
Copilotmini-page
andauthored
refactor: create AppPillSwitch + AppTheme, reduce UI duplication
Agent-Logs-Url: https://github.com/mini-page/XPensa/sessions/2ef999ef-0e0c-4ffa-b97f-d884c1899afd Co-authored-by: mini-page <146421384+mini-page@users.noreply.github.com>
1 parent 810c03f commit 28453f8

8 files changed

Lines changed: 164 additions & 176 deletions

File tree

lib/core/theme/app_theme.dart

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:flutter/material.dart';
2+
3+
import 'app_colors.dart';
4+
import 'app_tokens.dart';
5+
6+
/// Centralised [ThemeData] factory for XPensa.
7+
///
8+
/// Use [AppTheme.light] and [AppTheme.dark] instead of building
9+
/// [ThemeData] inline in `main.dart`. This enables global UI updates
10+
/// from a single file and simplifies dark / light theme toggling.
11+
class AppTheme {
12+
AppTheme._();
13+
14+
/// Light theme.
15+
static ThemeData light() {
16+
return ThemeData(
17+
scaffoldBackgroundColor: const Color(0xFFF3F7FC),
18+
colorScheme: ColorScheme.fromSeed(
19+
seedColor: AppColors.primaryBlue,
20+
primary: AppColors.primaryBlue,
21+
secondary: AppColors.danger,
22+
surface: Colors.white,
23+
),
24+
useMaterial3: true,
25+
textTheme: _textTheme(AppColors.textDark),
26+
);
27+
}
28+
29+
/// Dark theme.
30+
static ThemeData dark() {
31+
return ThemeData(
32+
scaffoldBackgroundColor: const Color(0xFF0E1626),
33+
colorScheme: ColorScheme.fromSeed(
34+
seedColor: AppColors.primaryBlueLight,
35+
brightness: Brightness.dark,
36+
primary: AppColors.primaryBlueLight,
37+
secondary: const Color(0xFFFF6C89),
38+
surface: const Color(0xFF182234),
39+
),
40+
useMaterial3: true,
41+
textTheme: _textTheme(Colors.white),
42+
);
43+
}
44+
45+
static TextTheme _textTheme(Color defaultColor) {
46+
return TextTheme(
47+
headlineLarge: AppTextStyles.sectionHeading.copyWith(color: defaultColor),
48+
headlineMedium:
49+
AppTextStyles.sectionHeading.copyWith(color: defaultColor, fontSize: 18),
50+
titleMedium: AppTextStyles.bodyStrong.copyWith(color: defaultColor),
51+
bodyMedium: AppTextStyles.bodyMuted,
52+
labelSmall: AppTextStyles.eyebrowOnDark,
53+
);
54+
}
55+
}

lib/core/theme/index.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export 'app_colors.dart';
22
export 'app_tokens.dart';
3+
export 'app_theme.dart';

lib/features/expense/presentation/screens/accounts/accounts_widgets.dart

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:intl/intl.dart';
44

55
import '../../../../../core/theme/app_colors.dart';
66
import '../../../../../core/theme/app_tokens.dart';
7+
import '../../../../../shared/widgets/app_pill_switch.dart';
78
import '../../../data/models/account_model.dart';
89
import '../../provider/account_providers.dart';
910
import '../../provider/expense_providers.dart';
@@ -33,82 +34,10 @@ class AccountsToolsTabView extends StatelessWidget {
3334
}
3435

3536
/// Two-option pill toggle used for the Accounts / Tools tab.
36-
class AccountsPillSwitch extends StatelessWidget {
37-
const AccountsPillSwitch({
38-
super.key,
39-
required this.leftLabel,
40-
required this.rightLabel,
41-
required this.isRightSelected,
42-
required this.onChanged,
43-
});
44-
45-
final String leftLabel;
46-
final String rightLabel;
47-
final bool isRightSelected;
48-
final ValueChanged<bool> onChanged;
49-
50-
@override
51-
Widget build(BuildContext context) {
52-
return Container(
53-
padding: const EdgeInsets.all(4),
54-
decoration: BoxDecoration(
55-
color: AppColors.backgroundLight,
56-
borderRadius: BorderRadius.circular(22),
57-
),
58-
child: Row(
59-
children: <Widget>[
60-
_AccountsSwitchOption(
61-
label: leftLabel,
62-
isSelected: !isRightSelected,
63-
onTap: () => onChanged(false),
64-
),
65-
_AccountsSwitchOption(
66-
label: rightLabel,
67-
isSelected: isRightSelected,
68-
onTap: () => onChanged(true),
69-
),
70-
],
71-
),
72-
);
73-
}
74-
}
75-
76-
class _AccountsSwitchOption extends StatelessWidget {
77-
const _AccountsSwitchOption({
78-
required this.label,
79-
required this.isSelected,
80-
required this.onTap,
81-
});
82-
83-
final String label;
84-
final bool isSelected;
85-
final VoidCallback onTap;
86-
87-
@override
88-
Widget build(BuildContext context) {
89-
return Expanded(
90-
child: GestureDetector(
91-
onTap: onTap,
92-
child: Container(
93-
padding: const EdgeInsets.symmetric(vertical: 14),
94-
decoration: BoxDecoration(
95-
color: isSelected ? AppColors.primaryBlue : Colors.transparent,
96-
borderRadius: BorderRadius.circular(18),
97-
),
98-
child: Text(
99-
label,
100-
textAlign: TextAlign.center,
101-
style: TextStyle(
102-
color: isSelected ? Colors.white : AppColors.textMuted,
103-
fontWeight: FontWeight.w800,
104-
fontSize: 16,
105-
),
106-
),
107-
),
108-
),
109-
);
110-
}
111-
}
37+
///
38+
/// Thin wrapper around the shared [AppPillSwitch] that preserves the
39+
/// existing call-site API.
40+
typedef AccountsPillSwitch = AppPillSwitch;
11241

11342
/// A labelled summary chip shown in the net-worth hero card.
11443
class AccountsSummaryChip extends StatelessWidget {

lib/features/expense/presentation/screens/categories/categories_widgets.dart

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,13 @@
11
import 'package:flutter/material.dart';
22

33
import '../../../../../core/theme/app_colors.dart';
4+
import '../../../../../shared/widgets/app_pill_switch.dart';
45

56
/// Two-option pill toggle for switching between expense/income categories.
6-
class CategoriesPillSwitch extends StatelessWidget {
7-
const CategoriesPillSwitch({
8-
super.key,
9-
required this.leftLabel,
10-
required this.rightLabel,
11-
required this.isRightSelected,
12-
required this.onChanged,
13-
});
14-
15-
final String leftLabel;
16-
final String rightLabel;
17-
final bool isRightSelected;
18-
final ValueChanged<bool> onChanged;
19-
20-
@override
21-
Widget build(BuildContext context) {
22-
return Container(
23-
padding: const EdgeInsets.all(4),
24-
decoration: BoxDecoration(
25-
color: AppColors.backgroundLight,
26-
borderRadius: BorderRadius.circular(22),
27-
),
28-
child: Row(
29-
children: <Widget>[
30-
_CategoriesSwitchOption(
31-
label: leftLabel,
32-
isSelected: !isRightSelected,
33-
onTap: () => onChanged(false),
34-
),
35-
_CategoriesSwitchOption(
36-
label: rightLabel,
37-
isSelected: isRightSelected,
38-
onTap: () => onChanged(true),
39-
),
40-
],
41-
),
42-
);
43-
}
44-
}
45-
46-
class _CategoriesSwitchOption extends StatelessWidget {
47-
const _CategoriesSwitchOption({
48-
required this.label,
49-
required this.isSelected,
50-
required this.onTap,
51-
});
52-
53-
final String label;
54-
final bool isSelected;
55-
final VoidCallback onTap;
56-
57-
@override
58-
Widget build(BuildContext context) {
59-
return Expanded(
60-
child: GestureDetector(
61-
onTap: onTap,
62-
child: Container(
63-
padding: const EdgeInsets.symmetric(vertical: 14),
64-
decoration: BoxDecoration(
65-
color: isSelected ? AppColors.primaryBlue : Colors.transparent,
66-
borderRadius: BorderRadius.circular(18),
67-
),
68-
child: Text(
69-
label,
70-
textAlign: TextAlign.center,
71-
style: TextStyle(
72-
color: isSelected ? Colors.white : AppColors.textMuted,
73-
fontWeight: FontWeight.w800,
74-
fontSize: 16,
75-
),
76-
),
77-
),
78-
),
79-
);
80-
}
81-
}
7+
///
8+
/// Thin wrapper around the shared [AppPillSwitch] that preserves the
9+
/// existing call-site API.
10+
typedef CategoriesPillSwitch = AppPillSwitch;
8211

8312
/// A single category grid cell showing spend amount and optional budget detail.
8413
class CategoryGridCard extends StatelessWidget {

lib/main.dart

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
33
import 'package:flutter_riverpod/flutter_riverpod.dart';
44
import 'package:workmanager/workmanager.dart';
55

6-
import 'core/theme/app_colors.dart';
6+
import 'core/theme/app_theme.dart';
77
import 'core/utils/background_backup.dart';
88
import 'core/utils/hive_bootstrap.dart';
99
import 'features/expense/presentation/provider/preferences_providers.dart';
@@ -40,27 +40,8 @@ class XPensaApp extends ConsumerWidget {
4040
return MaterialApp(
4141
debugShowCheckedModeBanner: false,
4242
title: 'XPensa',
43-
theme: ThemeData(
44-
scaffoldBackgroundColor: const Color(0xFFF3F7FC),
45-
colorScheme: ColorScheme.fromSeed(
46-
seedColor: AppColors.primaryBlue,
47-
primary: AppColors.primaryBlue,
48-
secondary: AppColors.danger,
49-
surface: Colors.white,
50-
),
51-
useMaterial3: true,
52-
),
53-
darkTheme: ThemeData(
54-
scaffoldBackgroundColor: const Color(0xFF0E1626),
55-
colorScheme: ColorScheme.fromSeed(
56-
seedColor: AppColors.primaryBlueLight,
57-
brightness: Brightness.dark,
58-
primary: AppColors.primaryBlueLight,
59-
secondary: const Color(0xFFFF6C89),
60-
surface: const Color(0xFF182234),
61-
),
62-
useMaterial3: true,
63-
),
43+
theme: AppTheme.light(),
44+
darkTheme: AppTheme.dark(),
6445
themeMode: themeMode,
6546
home: preferencesAsync.when(
6647
data: (_) =>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../../core/theme/app_colors.dart';
4+
5+
/// A two-option pill-shaped toggle widget.
6+
///
7+
/// Used for mutually exclusive binary choices (e.g. Accounts / Tools,
8+
/// Expense / Income categories). Replaces the previously duplicated
9+
/// `AccountsPillSwitch` and `CategoriesPillSwitch` widgets.
10+
class AppPillSwitch extends StatelessWidget {
11+
const AppPillSwitch({
12+
super.key,
13+
required this.leftLabel,
14+
required this.rightLabel,
15+
required this.isRightSelected,
16+
required this.onChanged,
17+
});
18+
19+
final String leftLabel;
20+
final String rightLabel;
21+
final bool isRightSelected;
22+
final ValueChanged<bool> onChanged;
23+
24+
@override
25+
Widget build(BuildContext context) {
26+
return Container(
27+
padding: const EdgeInsets.all(4),
28+
decoration: BoxDecoration(
29+
color: AppColors.backgroundLight,
30+
borderRadius: BorderRadius.circular(22),
31+
),
32+
child: Row(
33+
children: <Widget>[
34+
_PillOption(
35+
label: leftLabel,
36+
isSelected: !isRightSelected,
37+
onTap: () => onChanged(false),
38+
),
39+
_PillOption(
40+
label: rightLabel,
41+
isSelected: isRightSelected,
42+
onTap: () => onChanged(true),
43+
),
44+
],
45+
),
46+
);
47+
}
48+
}
49+
50+
class _PillOption extends StatelessWidget {
51+
const _PillOption({
52+
required this.label,
53+
required this.isSelected,
54+
required this.onTap,
55+
});
56+
57+
final String label;
58+
final bool isSelected;
59+
final VoidCallback onTap;
60+
61+
@override
62+
Widget build(BuildContext context) {
63+
return Expanded(
64+
child: GestureDetector(
65+
onTap: onTap,
66+
child: Container(
67+
padding: const EdgeInsets.symmetric(vertical: 14),
68+
decoration: BoxDecoration(
69+
color: isSelected ? AppColors.primaryBlue : Colors.transparent,
70+
borderRadius: BorderRadius.circular(18),
71+
),
72+
child: Text(
73+
label,
74+
textAlign: TextAlign.center,
75+
style: TextStyle(
76+
color: isSelected ? Colors.white : AppColors.textMuted,
77+
fontWeight: FontWeight.w800,
78+
fontSize: 16,
79+
),
80+
),
81+
),
82+
),
83+
);
84+
}
85+
}

lib/shared/widgets/index.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export 'floating_nav_bar.dart';
22
export 'placeholder_screen.dart';
3+
export 'app_pill_switch.dart';

0 commit comments

Comments
 (0)