diff --git a/lib/app/app.dart b/lib/app/app.dart index f58f14a..69c33cd 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -7,6 +7,7 @@ import '../routing/app_router.dart'; import '../theme/app_theme.dart'; import '../theme/yanting_text.dart'; import '../theme/yanting_shad_theme.dart'; +import '../theme/theme_controller.dart'; class ReportNotebooklmApp extends ConsumerWidget { const ReportNotebooklmApp({super.key}); @@ -14,6 +15,7 @@ class ReportNotebooklmApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); + final themeMode = ref.watch(themeModeProvider); final dmSansStyle = GoogleFonts.dmSans().copyWith( fontFamilyFallback: YantingText.fontFallback, ); @@ -23,9 +25,10 @@ class ReportNotebooklmApp extends ConsumerWidget { debugShowCheckedModeBanner: false, theme: buildYantingShadTheme(), darkTheme: buildYantingDarkShadTheme(), + themeMode: themeMode, routerConfig: router, scrollBehavior: const ShadScrollBehavior(), - materialThemeBuilder: (context, theme) => buildAppTheme(), + materialThemeBuilder: (context, theme) => buildAppTheme(theme.brightness), builder: (context, child) { return DefaultTextStyle.merge( style: TextStyle(fontFamilyFallback: dmSansStyle.fontFamilyFallback), diff --git a/lib/features/detail/modules/renderer_registry.dart b/lib/features/detail/modules/renderer_registry.dart index 838de71..f67c7cc 100644 --- a/lib/features/detail/modules/renderer_registry.dart +++ b/lib/features/detail/modules/renderer_registry.dart @@ -325,6 +325,7 @@ class _CoreInsights extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final points = asMapList(payload['points']); if (points.isEmpty) return _TextLines(payload: payload); return Column( @@ -335,8 +336,8 @@ class _CoreInsights extends StatelessWidget { margin: const EdgeInsets.only(bottom: YantingSpacing.x3), padding: const EdgeInsets.all(YantingSpacing.x3), decoration: BoxDecoration( - color: YantingColors.background, - border: Border.all(color: YantingColors.border), + color: colors.background, + border: Border.all(color: colors.border), borderRadius: BorderRadius.circular(YantingRadius.md), ), child: Column( @@ -364,6 +365,7 @@ class _SourceCompliance extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final institution = report?.institution; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -403,15 +405,15 @@ class _SourceCompliance extends StatelessWidget { const SizedBox(height: YantingSpacing.x3), DecoratedBox( decoration: BoxDecoration( - color: YantingColors.background, - border: Border.all(color: YantingColors.border), + color: colors.background, + border: Border.all(color: colors.border), borderRadius: BorderRadius.circular(YantingRadius.md), ), child: Padding( padding: const EdgeInsets.all(YantingSpacing.x3), child: Text( asString(payload['disclaimer'], '本内容为公开/授权研报的结构化解读,不构成投资建议。'), - style: YantingText.meta.copyWith(color: YantingColors.warning), + style: YantingText.meta.copyWith(color: colors.warning), ), ), ), @@ -428,6 +430,7 @@ class _InfoLine extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; if (value.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: YantingSpacing.x2), @@ -437,7 +440,7 @@ class _InfoLine extends StatelessWidget { Text( label, style: YantingText.badge.copyWith( - color: YantingColors.mutedForeground, + color: colors.mutedForeground, ), ), const SizedBox(height: YantingSpacing.x1), @@ -496,10 +499,11 @@ class _InstitutionModule extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final name = asString(payload['name_cn'], report?.institution.nameCn ?? ''); return Row( children: [ - const Icon(AppIcons.bank, color: YantingColors.foreground), + Icon(AppIcons.bank, color: colors.foreground), const SizedBox(width: YantingSpacing.x2), Expanded(child: Text(name, style: YantingText.body)), Text( @@ -551,6 +555,7 @@ class _KeyDataModule extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; if (compact) return _Preview(payload: payload); final rows = asMapList(payload['rows']); return Column( @@ -561,7 +566,7 @@ class _KeyDataModule extends StatelessWidget { margin: const EdgeInsets.only(bottom: YantingSpacing.x3), padding: const EdgeInsets.all(YantingSpacing.x3), decoration: BoxDecoration( - color: YantingColors.secondary, + color: colors.secondary, borderRadius: BorderRadius.circular(YantingRadius.md), ), child: Row( @@ -575,9 +580,7 @@ class _KeyDataModule extends StatelessWidget { const SizedBox(height: 6), Text( asString(row['judgment'], asString(row['importance'])), - style: YantingText.body.copyWith( - color: YantingColors.foreground, - ), + style: YantingText.body.copyWith(color: colors.foreground), ), ], ), @@ -630,6 +633,7 @@ class _TimelineEntry extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -640,8 +644,8 @@ class _TimelineEntry extends StatelessWidget { width: 9, height: 9, margin: const EdgeInsets.only(top: 6), - decoration: const BoxDecoration( - color: YantingColors.primary, + decoration: BoxDecoration( + color: colors.primary, shape: BoxShape.circle, ), ), @@ -650,7 +654,7 @@ class _TimelineEntry extends StatelessWidget { child: Container( width: 1, margin: const EdgeInsets.symmetric(vertical: 4), - color: YantingColors.border, + color: colors.border, ), ), ], @@ -666,7 +670,7 @@ class _TimelineEntry extends StatelessWidget { Text( asString(event['date']), style: YantingText.meta.copyWith( - color: YantingColors.foreground, + color: colors.foreground, fontWeight: FontWeight.w600, ), ), @@ -822,6 +826,7 @@ class _DifferentiatedViewModule extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; if (compact) return _Preview(payload: payload); final items = asMapList(payload['divergences']); return Column( @@ -842,7 +847,7 @@ class _DifferentiatedViewModule extends StatelessWidget { Text( '常见观点', style: YantingText.badge.copyWith( - color: YantingColors.mutedForeground, + color: colors.mutedForeground, ), ), const SizedBox(height: YantingSpacing.x1), @@ -856,7 +861,7 @@ class _DifferentiatedViewModule extends StatelessWidget { Text( '报告观点', style: YantingText.badge.copyWith( - color: YantingColors.foreground, + color: colors.foreground, ), ), const SizedBox(height: YantingSpacing.x1), @@ -881,6 +886,7 @@ class _WeaknessesModule extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; if (compact) return _Preview(payload: payload); final items = asMapList(payload['items']); final verificationNotes = asStringList(payload['verification_notes']); @@ -916,7 +922,7 @@ class _WeaknessesModule extends StatelessWidget { const SizedBox(height: YantingSpacing.x2), DecoratedBox( decoration: BoxDecoration( - color: const Color(0x109A6500), + color: colors.warningSoft.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(YantingRadius.sm), ), child: Padding( @@ -926,9 +932,7 @@ class _WeaknessesModule extends StatelessWidget { children: [ Text( '需要继续验证', - style: YantingText.badge.copyWith( - color: YantingColors.warning, - ), + style: YantingText.badge.copyWith(color: colors.warning), ), const SizedBox(height: YantingSpacing.x1), for (final note diff --git a/lib/features/detail/report_detail_page.dart b/lib/features/detail/report_detail_page.dart index 748ba15..650e52c 100644 --- a/lib/features/detail/report_detail_page.dart +++ b/lib/features/detail/report_detail_page.dart @@ -119,6 +119,7 @@ class _ReportDetailContent extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return ListView( padding: const EdgeInsets.fromLTRB( YantingSpacing.x4, @@ -128,8 +129,8 @@ class _ReportDetailContent extends StatelessWidget { ), children: [ AppCard( - color: YantingColors.brandSoft, - borderColor: YantingColors.brandSoftBorder, + color: colors.brandSoft, + borderColor: colors.brandSoftBorder, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/listen/listen_page.dart b/lib/features/listen/listen_page.dart index 6316911..9414877 100644 --- a/lib/features/listen/listen_page.dart +++ b/lib/features/listen/listen_page.dart @@ -72,6 +72,7 @@ class _ContinueListeningCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return AppCard( onTap: onPlay, child: Column( @@ -97,13 +98,13 @@ class _ContinueListeningCard extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [ - Text( - item.institution.nameCn, - style: YantingText.meta.copyWith( - color: YantingColors.foreground, - fontWeight: FontWeight.w500, - ), + Text( + item.institution.nameCn, + style: YantingText.meta.copyWith( + color: colors.foreground, + fontWeight: FontWeight.w500, ), + ), Text('·', style: YantingText.meta), Text( '全长 ${formatDuration(item.durationSec)}', @@ -153,6 +154,7 @@ class _AudioListCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return AppCard( padding: const EdgeInsets.all(14), onTap: onPlay, @@ -162,12 +164,12 @@ class _AudioListCard extends StatelessWidget { width: 56, height: 56, decoration: BoxDecoration( - color: YantingColors.secondary, + color: colors.secondary, borderRadius: BorderRadius.circular(YantingRadius.xl), ), - child: const Icon( + child: Icon( AppIcons.music, - color: YantingColors.mutedForeground, + color: colors.mutedForeground, size: 24, ), ), @@ -211,8 +213,9 @@ class _PlayControlButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return Material( - color: YantingColors.primary, + color: colors.primary, borderRadius: BorderRadius.circular(YantingRadius.pill), child: InkWell( onTap: onPressed, @@ -221,7 +224,7 @@ class _PlayControlButton extends StatelessWidget { dimension: size, child: Icon( AppIcons.play, - color: YantingColors.primaryForeground, + color: colors.primaryForeground, size: iconSize, ), ), diff --git a/lib/features/profile/profile_page.dart b/lib/features/profile/profile_page.dart index 8f54bbb..a401c66 100644 --- a/lib/features/profile/profile_page.dart +++ b/lib/features/profile/profile_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../theme/app_icons.dart'; +import '../../routing/app_routes.dart'; import '../../theme/yanting_text.dart'; import '../../theme/yanting_tokens.dart'; import '../../widgets/app_buttons.dart'; @@ -17,6 +20,7 @@ class ProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return ListView( padding: const EdgeInsets.fromLTRB( YantingSpacing.screenX, @@ -27,13 +31,13 @@ class ProfilePage extends StatelessWidget { children: [ const PageHeader(title: '我的'), AppCard( - color: YantingColors.secondary, + color: colors.secondary, child: Row( children: [ CircleAvatar( radius: 27, - backgroundColor: YantingColors.background, - foregroundColor: YantingColors.mutedForeground, + backgroundColor: colors.background, + foregroundColor: colors.mutedForeground, child: const Icon(AppIcons.user, size: 28), ), const SizedBox(width: 15), @@ -43,12 +47,18 @@ class ProfilePage extends StatelessWidget { children: [ Text( '未登录', - style: YantingText.cardTitle.copyWith(fontSize: 18), + style: YantingText.cardTitle.copyWith( + fontSize: 18, + color: colors.foreground, + ), ), const SizedBox(height: 5), Text( '登录后同步收藏、历史和听单', - style: YantingText.meta.copyWith(height: 1.5), + style: YantingText.meta.copyWith( + height: 1.5, + color: colors.mutedForeground, + ), ), ], ), @@ -79,7 +89,7 @@ class ProfilePage extends StatelessWidget { _MenuRow( icon: AppIcons.settings, title: '设置', - onTap: () => showAppToast(context, '设置待接入'), + onTap: () => context.push(AppRoutes.settings), ), _MenuRow( icon: AppIcons.fileList, @@ -95,7 +105,7 @@ class ProfilePage extends StatelessWidget { ), const SizedBox(height: YantingSpacing.x3), AppCard( - color: YantingColors.secondary, + color: colors.secondary, onTap: () => showOutboundSheet(context, title: '相关服务'), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -105,7 +115,7 @@ class ProfilePage extends StatelessWidget { Text( '了解相关服务', style: YantingText.body.copyWith( - color: YantingColors.foreground, + color: colors.foreground, fontWeight: FontWeight.w600, ), ), @@ -116,7 +126,10 @@ class ProfilePage extends StatelessWidget { const SizedBox(height: 6), Text( '与你关注主题相关的延伸服务,内容不构成投资建议。', - style: YantingText.meta.copyWith(height: 1.5), + style: YantingText.meta.copyWith( + height: 1.5, + color: colors.mutedForeground, + ), ), ], ), @@ -139,6 +152,7 @@ class _MenuGroup extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return AppCard( padding: EdgeInsets.zero, child: Column( @@ -146,11 +160,7 @@ class _MenuGroup extends StatelessWidget { for (var index = 0; index < children.length; index++) ...[ children[index], if (index != children.length - 1) - const Divider( - height: 1, - thickness: 1, - color: YantingColors.border, - ), + Divider(height: 1, thickness: 1, color: colors.border), ], ], ), @@ -173,19 +183,20 @@ class _MenuRow extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), child: Row( children: [ - Icon(icon, size: 20, color: YantingColors.foreground), + Icon(icon, size: 20, color: colors.foreground), const SizedBox(width: 13), Expanded(child: Text(title, style: YantingText.body)), if (trailing != null) DecoratedBox( decoration: BoxDecoration( - color: YantingColors.secondary, + color: colors.secondary, borderRadius: BorderRadius.circular(YantingRadius.pill), ), child: Padding( @@ -195,14 +206,17 @@ class _MenuRow extends StatelessWidget { ), child: Text( trailing!, - style: YantingText.meta.copyWith(fontSize: 11.5), + style: YantingText.meta.copyWith( + fontSize: 11.5, + color: colors.secondaryForeground, + ), ), ), ) else - const Icon( + Icon( AppIcons.arrowRight, - color: YantingColors.mutedForeground, + color: colors.mutedForeground, size: 20, ), ], diff --git a/lib/features/settings/settings_page.dart b/lib/features/settings/settings_page.dart new file mode 100644 index 0000000..8bfa14e --- /dev/null +++ b/lib/features/settings/settings_page.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../routing/app_routes.dart'; +import '../../theme/app_icons.dart'; +import '../../theme/theme_controller.dart'; +import '../../theme/yanting_text.dart'; +import '../../theme/yanting_tokens.dart'; +import '../../widgets/app_card.dart'; +import '../../widgets/page_header.dart'; +import '../../widgets/sheets.dart'; + +class SettingsPage extends ConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(themeModeProvider); + final scheme = ShadTheme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(AppIcons.arrowLeft), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(AppRoutes.home); + } + }, + ), + title: const Text('设置 · Settings'), + ), + body: ListView( + padding: const EdgeInsets.fromLTRB( + YantingSpacing.screenX, + 4, + YantingSpacing.screenX, + 20, + ), + children: [ + const PageHeader(title: '设置', subtitle: '统一外观、主题和常用入口'), + Text('外观', style: YantingText.sectionTitle), + const SizedBox(height: 12), + AppCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + _ThemeModeTile( + label: '跟随系统', + description: '按系统深浅色自动切换', + selected: themeMode == ThemeMode.system, + onTap: () => ref + .read(themeModeProvider.notifier) + .setMode(ThemeMode.system), + ), + const Divider(height: 1, thickness: 1), + _ThemeModeTile( + label: '浅色', + description: '稳定的浅色展示模式', + selected: themeMode == ThemeMode.light, + onTap: () => ref + .read(themeModeProvider.notifier) + .setMode(ThemeMode.light), + ), + const Divider(height: 1, thickness: 1), + _ThemeModeTile( + label: '深色', + description: '适合低光环境阅读', + selected: themeMode == ThemeMode.dark, + onTap: () => ref + .read(themeModeProvider.notifier) + .setMode(ThemeMode.dark), + ), + ], + ), + ), + const SizedBox(height: YantingSpacing.x3), + Text('入口', style: YantingText.sectionTitle), + const SizedBox(height: 12), + AppCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + _LinkTile( + icon: Icons.description_outlined, + title: '用户协议', + onTap: () => showOutboundSheet(context, title: '用户协议'), + ), + const Divider(height: 1, thickness: 1), + _LinkTile( + icon: Icons.privacy_tip_outlined, + title: '隐私政策', + onTap: () => showOutboundSheet(context, title: '隐私政策'), + ), + ], + ), + ), + const SizedBox(height: YantingSpacing.x3), + Text('关于', style: YantingText.sectionTitle), + const SizedBox(height: 12), + AppCard( + color: scheme.secondary, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('研听', style: YantingText.cardTitle), + const SizedBox(height: 6), + Text( + '全球机构研报中文解读', + style: YantingText.meta.copyWith(fontSize: 12.5), + ), + const SizedBox(height: 12), + Text( + '主题、按钮、卡片和间距统一到 demo 的展示层基线。', + style: YantingText.body.copyWith(fontSize: 14), + ), + const SizedBox(height: 14), + Text( + '当前版本以本地构建信息为准,发布时再注入正式版本号。', + style: YantingText.meta.copyWith(fontSize: 12), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _ThemeModeTile extends StatelessWidget { + const _ThemeModeTile({ + required this.label, + required this.description, + required this.selected, + required this.onTap, + }); + + final String label; + final String description; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final scheme = ShadTheme.of(context).colorScheme; + final foreground = selected ? scheme.background : scheme.foreground; + final muted = scheme.mutedForeground; + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: YantingText.body.copyWith( + color: scheme.foreground, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: YantingText.meta.copyWith( + color: muted, + fontSize: 12.5, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + DecoratedBox( + decoration: BoxDecoration( + color: selected ? scheme.foreground : scheme.secondary, + borderRadius: BorderRadius.circular(YantingRadius.pill), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), + child: Icon( + selected ? Icons.check : Icons.radio_button_unchecked, + size: 16, + color: foreground, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _LinkTile extends StatelessWidget { + const _LinkTile({ + required this.icon, + required this.title, + required this.onTap, + }); + + final IconData icon; + final String title; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + child: Row( + children: [ + Icon( + icon, + size: 20, + color: ShadTheme.of(context).colorScheme.foreground, + ), + const SizedBox(width: 13), + Expanded( + child: Text( + title, + style: YantingText.body.copyWith( + color: ShadTheme.of(context).colorScheme.foreground, + ), + ), + ), + const Icon(AppIcons.arrowRight, size: 18), + ], + ), + ), + ); + } +} diff --git a/lib/features/shared/report_card_widget.dart b/lib/features/shared/report_card_widget.dart index a90f48e..d93342d 100644 --- a/lib/features/shared/report_card_widget.dart +++ b/lib/features/shared/report_card_widget.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/models/models.dart'; import '../../theme/app_icons.dart'; import '../../theme/yanting_text.dart'; -import '../../theme/yanting_tokens.dart'; import '../../theme/wise_tokens.dart'; import '../../widgets/app_buttons.dart'; import '../../widgets/app_card.dart'; @@ -27,6 +27,7 @@ class ReportCardWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final child = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -66,7 +67,7 @@ class ReportCardWidget extends StatelessWidget { maxLines: 2, overflow: TextOverflow.ellipsis, style: YantingText.body.copyWith( - color: YantingColors.mutedForeground, + color: colors.mutedForeground, fontSize: hero ? null : 14, ), ), @@ -84,7 +85,7 @@ class ReportCardWidget extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: YantingText.meta.copyWith( - color: YantingColors.foreground, + color: colors.foreground, fontWeight: FontWeight.w500, ), ), @@ -122,11 +123,12 @@ class _MetaDot extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return Container( width: 3, height: 3, - decoration: const BoxDecoration( - color: YantingColors.mutedForeground, + decoration: BoxDecoration( + color: colors.mutedForeground, shape: BoxShape.circle, ), ); diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index c148709..88c36cb 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -12,6 +12,7 @@ import '../features/institutions/institutions_page.dart'; import '../features/listen/listen_page.dart'; import '../features/profile/profile_page.dart'; import '../features/reports/reports_page.dart'; +import '../features/settings/settings_page.dart'; import '../features/shell_page.dart'; import 'app_routes.dart'; @@ -154,6 +155,10 @@ final routerProvider = Provider((ref) { ); }, ), + GoRoute( + path: AppRoutes.settings, + builder: (context, state) => const SettingsPage(), + ), ], ); }); diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index 939a72d..8185c9c 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -12,6 +12,7 @@ abstract final class AppRoutes { static const institutions = '/institutions'; static const listen = '/listen'; static const profile = '/profile'; + static const settings = '/settings'; static const reportDetail = '/reports/:id'; static const institutionDetail = '/institutions/:id'; diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index d896dac..0ff5df2 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -3,85 +3,119 @@ import 'package:flutter/material.dart'; import 'yanting_text.dart'; import 'yanting_tokens.dart'; -ThemeData buildAppTheme() { +ThemeData buildAppTheme(Brightness brightness) { + final primary = brightness == Brightness.dark + ? YantingDarkColors.primary + : YantingColors.primary; + final primaryForeground = brightness == Brightness.dark + ? YantingDarkColors.primaryForeground + : YantingColors.primaryForeground; + final secondary = brightness == Brightness.dark + ? YantingDarkColors.secondary + : YantingColors.secondary; + final secondaryForeground = brightness == Brightness.dark + ? YantingDarkColors.secondaryForeground + : YantingColors.secondaryForeground; + final link = brightness == Brightness.dark + ? YantingDarkColors.link + : YantingColors.link; + final card = brightness == Brightness.dark + ? YantingDarkColors.card + : YantingColors.card; + final foreground = brightness == Brightness.dark + ? YantingDarkColors.foreground + : YantingColors.foreground; + final destructive = brightness == Brightness.dark + ? YantingDarkColors.destructive + : YantingColors.destructive; + final border = brightness == Brightness.dark + ? YantingDarkColors.border + : YantingColors.border; + final background = brightness == Brightness.dark + ? YantingDarkColors.background + : YantingColors.background; + final mutedForeground = brightness == Brightness.dark + ? YantingDarkColors.mutedForeground + : YantingColors.mutedForeground; + final input = brightness == Brightness.dark + ? YantingDarkColors.input + : YantingColors.input; final scheme = ColorScheme.fromSeed( - seedColor: YantingColors.primary, - primary: YantingColors.primary, - onPrimary: YantingColors.primaryForeground, - secondary: YantingColors.secondary, - onSecondary: YantingColors.secondaryForeground, - tertiary: YantingColors.link, - surface: YantingColors.card, - onSurface: YantingColors.foreground, - error: YantingColors.destructive, - outline: YantingColors.border, + seedColor: primary, + brightness: brightness, + primary: primary, + onPrimary: primaryForeground, + secondary: secondary, + onSecondary: secondaryForeground, + tertiary: link, + surface: card, + onSurface: foreground, + error: destructive, + outline: border, ); return ThemeData( useMaterial3: true, colorScheme: scheme, fontFamily: YantingText.fontFamily, fontFamilyFallback: YantingText.fontFallback, - scaffoldBackgroundColor: YantingColors.background, - appBarTheme: const AppBarTheme( - backgroundColor: YantingColors.background, - foregroundColor: YantingColors.foreground, + brightness: brightness, + scaffoldBackgroundColor: background, + appBarTheme: AppBarTheme( + backgroundColor: background, + foregroundColor: foreground, elevation: 0, centerTitle: false, titleTextStyle: YantingText.sectionTitle, surfaceTintColor: Colors.transparent, ), - cardTheme: const CardThemeData( - color: YantingColors.card, + cardTheme: CardThemeData( + color: card, elevation: 0, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(YantingRadius.xl)), - side: BorderSide(color: YantingColors.border), + borderRadius: const BorderRadius.all(Radius.circular(YantingRadius.xl)), + side: BorderSide(color: border), ), ), - dividerTheme: const DividerThemeData( - color: YantingColors.border, + dividerTheme: DividerThemeData( + color: border, thickness: 1, space: 1, ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: YantingColors.background, - hintStyle: YantingText.body.copyWith( - color: YantingColors.mutedForeground, - ), + fillColor: background, + hintStyle: YantingText.body.copyWith(color: mutedForeground), contentPadding: const EdgeInsets.symmetric(horizontal: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(YantingRadius.md), - borderSide: const BorderSide(color: YantingColors.input), + borderSide: BorderSide(color: input), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(YantingRadius.md), - borderSide: const BorderSide(color: YantingColors.input), + borderSide: BorderSide(color: input), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(YantingRadius.md), - borderSide: const BorderSide(color: YantingColors.foreground), + borderSide: BorderSide(color: foreground), ), ), snackBarTheme: SnackBarThemeData( - backgroundColor: YantingColors.foreground, - contentTextStyle: YantingText.body.copyWith( - color: YantingColors.background, - ), + backgroundColor: foreground, + contentTextStyle: YantingText.body.copyWith(color: background), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(YantingRadius.md), ), ), navigationBarTheme: NavigationBarThemeData( - backgroundColor: YantingColors.background, + backgroundColor: background, indicatorColor: Colors.transparent, labelTextStyle: WidgetStateProperty.resolveWith( (states) => YantingText.meta.copyWith( color: states.contains(WidgetState.selected) - ? YantingColors.foreground - : YantingColors.mutedForeground, + ? foreground + : mutedForeground, fontSize: 11, fontWeight: states.contains(WidgetState.selected) ? FontWeight.w600 @@ -91,8 +125,8 @@ ThemeData buildAppTheme() { iconTheme: WidgetStateProperty.resolveWith( (states) => IconThemeData( color: states.contains(WidgetState.selected) - ? YantingColors.foreground - : YantingColors.mutedForeground, + ? foreground + : mutedForeground, ), ), ), diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 8ae46e8..9b9de2f 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,5 +1,6 @@ export 'app_theme.dart'; export 'yanting_shad_theme.dart'; +export 'theme_controller.dart'; export 'yanting_text.dart'; export 'yanting_tokens.dart'; export 'wise_tokens.dart'; diff --git a/lib/theme/theme_controller.dart b/lib/theme/theme_controller.dart new file mode 100644 index 0000000..b993bd4 --- /dev/null +++ b/lib/theme/theme_controller.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +const _themeModeKey = 'theme_mode'; + +final themeModeProvider = StateNotifierProvider( + (ref) => ThemeModeController(), +); + +class ThemeModeController extends StateNotifier { + ThemeModeController() : super(ThemeMode.system) { + unawaited(_loadSavedMode()); + } + + Future _loadSavedMode() async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getString(_themeModeKey); + if (raw == null) return; + state = _decode(raw); + } + + Future setMode(ThemeMode mode) async { + state = mode; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_themeModeKey, _encode(mode)); + } + + ThemeMode _decode(String raw) => switch (raw) { + 'light' => ThemeMode.light, + 'dark' => ThemeMode.dark, + _ => ThemeMode.system, + }; + + String _encode(ThemeMode mode) => switch (mode) { + ThemeMode.light => 'light', + ThemeMode.dark => 'dark', + ThemeMode.system => 'system', + }; +} diff --git a/lib/theme/yanting_shad_theme.dart b/lib/theme/yanting_shad_theme.dart index 87991e8..1651359 100644 --- a/lib/theme/yanting_shad_theme.dart +++ b/lib/theme/yanting_shad_theme.dart @@ -5,126 +5,107 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import 'yanting_text.dart'; import 'yanting_tokens.dart'; -const _lightShadColors = ShadColorScheme( - background: YantingColors.background, - foreground: YantingColors.foreground, - card: YantingColors.card, - cardForeground: YantingColors.foreground, - popover: YantingColors.card, - popoverForeground: YantingColors.foreground, - primary: YantingColors.primary, - primaryForeground: YantingColors.primaryForeground, - secondary: YantingColors.secondary, - secondaryForeground: YantingColors.secondaryForeground, - muted: YantingColors.muted, - mutedForeground: YantingColors.mutedForeground, - accent: YantingColors.brandSoft, - accentForeground: YantingColors.primaryForeground, - destructive: YantingColors.destructive, - destructiveForeground: YantingColors.background, - border: YantingColors.border, - input: YantingColors.input, - ring: YantingColors.primary, - selection: YantingColors.foreground, - custom: { - 'brandSoft': YantingColors.brandSoft, - 'brandSoftBorder': YantingColors.brandSoftBorder, - 'link': YantingColors.link, - 'chart2': YantingColors.chart2, - 'warning': YantingColors.warning, - }, -); +ShadThemeData buildYantingShadTheme() => + _buildShadTheme(brightness: Brightness.light); -const _darkShadColors = ShadColorScheme( - background: Color(0xFF0F0F0F), - foreground: Color(0xFFF0F0F0), - card: Color(0xFF1A1A1A), - cardForeground: Color(0xFFF0F0F0), - popover: Color(0xFF1A1A1A), - popoverForeground: Color(0xFFF0F0F0), - primary: YantingColors.primary, - primaryForeground: Color(0xFF0F1A00), - secondary: Color(0xFF1F1F23), - secondaryForeground: Color(0xFFF0F0F0), - muted: Color(0xFF1A1A1A), - mutedForeground: Color(0xFFA1A1AA), - accent: Color(0xFF1C2B00), - accentForeground: YantingColors.primary, - destructive: YantingColors.destructive, - destructiveForeground: YantingColors.background, - border: Color(0xFF2A2A2A), - input: Color(0xFF2A2A2A), - ring: YantingColors.primary, - selection: Color(0xFFF0F0F0), - custom: { - 'brandSoft': Color(0xFF1C2B00), - 'brandSoftBorder': Color(0xFF304800), - 'link': YantingColors.link, - 'chart2': YantingColors.chart2, - 'warning': YantingColors.warning, - }, -); +ShadThemeData buildYantingDarkShadTheme() => + _buildShadTheme(brightness: Brightness.dark); + +ShadThemeData _buildShadTheme({required Brightness brightness}) { + final colors = brightness == Brightness.dark + ? ShadColorScheme( + background: YantingDarkColors.background, + foreground: YantingDarkColors.foreground, + card: YantingDarkColors.card, + cardForeground: YantingDarkColors.foreground, + popover: YantingDarkColors.card, + popoverForeground: YantingDarkColors.foreground, + primary: YantingDarkColors.primary, + primaryForeground: YantingDarkColors.primaryForeground, + secondary: YantingDarkColors.secondary, + secondaryForeground: YantingDarkColors.secondaryForeground, + muted: YantingDarkColors.muted, + mutedForeground: YantingDarkColors.mutedForeground, + accent: YantingDarkColors.brandSoft, + accentForeground: YantingDarkColors.primaryForeground, + destructive: YantingDarkColors.destructive, + destructiveForeground: YantingDarkColors.background, + border: YantingDarkColors.border, + input: YantingDarkColors.input, + ring: YantingDarkColors.primary, + selection: YantingDarkColors.foreground, + custom: { + 'brandSoft': YantingDarkColors.brandSoft, + 'brandSoftBorder': YantingDarkColors.brandSoftBorder, + 'link': YantingDarkColors.link, + 'chart2': YantingDarkColors.chart2, + 'warning': YantingDarkColors.warning, + 'warningSoft': YantingDarkColors.warningSoft, + 'warningSoftBorder': YantingDarkColors.warningSoftBorder, + 'warningSoftForeground': YantingDarkColors.warningSoftForeground, + }, + ) + : ShadColorScheme( + background: YantingColors.background, + foreground: YantingColors.foreground, + card: YantingColors.card, + cardForeground: YantingColors.foreground, + popover: YantingColors.card, + popoverForeground: YantingColors.foreground, + primary: YantingColors.primary, + primaryForeground: YantingColors.primaryForeground, + secondary: YantingColors.secondary, + secondaryForeground: YantingColors.secondaryForeground, + muted: YantingColors.muted, + mutedForeground: YantingColors.mutedForeground, + accent: YantingColors.brandSoft, + accentForeground: YantingColors.primaryForeground, + destructive: YantingColors.destructive, + destructiveForeground: YantingColors.background, + border: YantingColors.border, + input: YantingColors.input, + ring: YantingColors.primary, + selection: YantingColors.foreground, + custom: { + 'brandSoft': YantingColors.brandSoft, + 'brandSoftBorder': YantingColors.brandSoftBorder, + 'link': YantingColors.link, + 'chart2': YantingColors.chart2, + 'warning': YantingColors.warning, + 'warningSoft': YantingColors.warningSoft, + 'warningSoftBorder': YantingColors.warningSoftBorder, + 'warningSoftForeground': YantingColors.warningSoftForeground, + }, + ); + + final textTheme = ShadTextTheme( + family: YantingText.fontFamily, + h1Large: YantingText.appTitle.copyWith(color: colors.foreground), + h1: YantingText.appTitle.copyWith(color: colors.foreground), + h2: YantingText.sectionTitle.copyWith(color: colors.foreground), + h3: YantingText.cardTitle.copyWith(color: colors.foreground), + h4: YantingText.listTitle.copyWith(color: colors.foreground), + p: YantingText.body.copyWith(color: colors.foreground), + blockquote: YantingText.body.copyWith(color: colors.mutedForeground), + table: YantingText.meta.copyWith(color: colors.mutedForeground), + list: YantingText.body.copyWith(color: colors.foreground), + lead: YantingText.sub.copyWith(color: colors.mutedForeground), + large: YantingText.cardTitle.copyWith(color: colors.foreground), + small: YantingText.badge.copyWith(color: colors.mutedForeground), + muted: YantingText.meta.copyWith(color: colors.mutedForeground), + googleFontBuilder: GoogleFonts.dmSans, + ); -ShadThemeData buildYantingShadTheme() { return ShadThemeData( - brightness: Brightness.light, - colorScheme: _lightShadColors, + brightness: brightness, + colorScheme: colors, radius: BorderRadius.circular(YantingRadius.base), cardTheme: ShadCardTheme( padding: const EdgeInsets.all(YantingSpacing.cardPadding), radius: BorderRadius.circular(YantingRadius.xl), - border: ShadBorder.all(color: YantingColors.border), + border: ShadBorder.all(color: colors.border), shadows: const [], ), - textTheme: ShadTextTheme( - family: YantingText.fontFamily, - h1Large: YantingText.appTitle, - h1: YantingText.appTitle, - h2: YantingText.sectionTitle, - h3: YantingText.cardTitle, - h4: YantingText.listTitle, - p: YantingText.body, - blockquote: YantingText.body, - table: YantingText.meta, - list: YantingText.body, - lead: YantingText.sub, - large: YantingText.cardTitle, - small: YantingText.badge, - muted: YantingText.meta, - googleFontBuilder: GoogleFonts.dmSans, - ), - ); -} - -ShadThemeData buildYantingDarkShadTheme() { - return ShadThemeData( - brightness: Brightness.dark, - colorScheme: _darkShadColors, - radius: BorderRadius.circular(YantingRadius.base), - cardTheme: ShadCardTheme( - padding: const EdgeInsets.all(YantingSpacing.cardPadding), - radius: BorderRadius.circular(YantingRadius.xl), - border: ShadBorder.all(color: _darkShadColors.border), - shadows: const [], - ), - textTheme: ShadTextTheme( - family: YantingText.fontFamily, - h1Large: YantingText.appTitle.copyWith(color: _darkShadColors.foreground), - h1: YantingText.appTitle.copyWith(color: _darkShadColors.foreground), - h2: YantingText.sectionTitle.copyWith(color: _darkShadColors.foreground), - h3: YantingText.cardTitle.copyWith(color: _darkShadColors.foreground), - h4: YantingText.listTitle.copyWith(color: _darkShadColors.foreground), - p: YantingText.body.copyWith(color: _darkShadColors.foreground), - blockquote: YantingText.body.copyWith( - color: _darkShadColors.mutedForeground, - ), - table: YantingText.meta.copyWith(color: _darkShadColors.mutedForeground), - list: YantingText.body.copyWith(color: _darkShadColors.foreground), - lead: YantingText.sub.copyWith(color: _darkShadColors.mutedForeground), - large: YantingText.cardTitle.copyWith(color: _darkShadColors.foreground), - small: YantingText.badge.copyWith(color: _darkShadColors.mutedForeground), - muted: YantingText.meta.copyWith(color: _darkShadColors.mutedForeground), - googleFontBuilder: GoogleFonts.dmSans, - ), + textTheme: textTheme, ); } diff --git a/lib/theme/yanting_text.dart b/lib/theme/yanting_text.dart index 60fa04a..aaf6dfe 100644 --- a/lib/theme/yanting_text.dart +++ b/lib/theme/yanting_text.dart @@ -13,7 +13,6 @@ abstract final class YantingText { ]; static const appTitle = TextStyle( - color: YantingColors.foreground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 34, @@ -23,7 +22,6 @@ abstract final class YantingText { ); static const sectionTitle = TextStyle( - color: YantingColors.foreground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 22, @@ -33,7 +31,6 @@ abstract final class YantingText { ); static const cardTitle = TextStyle( - color: YantingColors.foreground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 19, @@ -43,7 +40,6 @@ abstract final class YantingText { ); static const listTitle = TextStyle( - color: YantingColors.foreground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 16.5, @@ -53,7 +49,6 @@ abstract final class YantingText { ); static const body = TextStyle( - color: YantingColors.foreground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 15, @@ -63,7 +58,6 @@ abstract final class YantingText { ); static const sub = TextStyle( - color: YantingColors.mutedForeground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 15, @@ -73,7 +67,6 @@ abstract final class YantingText { ); static const meta = TextStyle( - color: YantingColors.mutedForeground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 13, @@ -84,7 +77,6 @@ abstract final class YantingText { ); static const chip = TextStyle( - color: YantingColors.secondaryForeground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 15, @@ -94,7 +86,6 @@ abstract final class YantingText { ); static const badge = TextStyle( - color: YantingColors.mutedForeground, fontFamily: fontFamily, fontFamilyFallback: fontFallback, fontSize: 12, diff --git a/lib/theme/yanting_tokens.dart b/lib/theme/yanting_tokens.dart index 4c66350..e3d49f2 100644 --- a/lib/theme/yanting_tokens.dart +++ b/lib/theme/yanting_tokens.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; abstract final class YantingColors { static const background = Color(0xFFFFFFFF); @@ -14,6 +15,9 @@ abstract final class YantingColors { static const input = Color(0xFFE5E5E5); static const destructive = Color(0xFFEF4444); static const warning = Color(0xFF9A6500); + static const warningSoft = Color(0xFFFDE68A); + static const warningSoftBorder = Color(0xFFF5D26A); + static const warningSoftForeground = Color(0xFF7C4A00); static const chart2 = Color(0xFF84CC16); static const brandSoft = Color(0xFFECFCCB); static const brandSoftBorder = Color(0xFFD6F5A8); @@ -21,6 +25,30 @@ abstract final class YantingColors { static const canvas = background; } +abstract final class YantingDarkColors { + static const background = Color(0xFF09090B); + static const foreground = Color(0xFFF4F4F5); + static const card = Color(0xFF111113); + static const primary = Color(0xFF95E300); + static const primaryForeground = Color(0xFF0F1A00); + static const secondary = Color(0xFF1F1F23); + static const secondaryForeground = Color(0xFFE4E4E7); + static const muted = Color(0xFF18181B); + static const mutedForeground = Color(0xFFA1A1AA); + static const border = Color(0xFF27272A); + static const input = Color(0xFF27272A); + static const destructive = Color(0xFFF87171); + static const warning = Color(0xFFF59E0B); + static const warningSoft = Color(0xFF2A2412); + static const warningSoftBorder = Color(0xFF665113); + static const warningSoftForeground = Color(0xFFFBBF24); + static const chart2 = Color(0xFF84CC16); + static const brandSoft = Color(0xFF1C2B00); + static const brandSoftBorder = Color(0xFF304800); + static const link = Color(0xFF8AB4FF); + static const canvas = background; +} + abstract final class YantingSpacing { static const x1 = 4.0; static const x2 = 8.0; @@ -52,3 +80,15 @@ abstract final class YantingBorders { abstract final class YantingTypographyFeatures { static const tabularNums = [FontFeature.tabularFigures()]; } + +extension YantingShadColorSchemeX on ShadColorScheme { + Color get brandSoft => custom['brandSoft'] ?? accent; + Color get brandSoftBorder => custom['brandSoftBorder'] ?? border; + Color get link => custom['link'] ?? primary; + Color get warning => custom['warning'] ?? destructive; + Color get warningSoft => custom['warningSoft'] ?? muted; + Color get warningSoftBorder => custom['warningSoftBorder'] ?? border; + Color get warningSoftForeground => + custom['warningSoftForeground'] ?? foreground; + Color get chart2 => custom['chart2'] ?? primary; +} diff --git a/lib/widgets/app_buttons.dart b/lib/widgets/app_buttons.dart index 8862f30..ee9ede9 100644 --- a/lib/widgets/app_buttons.dart +++ b/lib/widgets/app_buttons.dart @@ -24,23 +24,24 @@ class AppButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final variant = switch (kind) { AppButtonKind.primary => ShadButtonVariant.primary, AppButtonKind.dark => ShadButtonVariant.primary, AppButtonKind.accent => ShadButtonVariant.secondary, AppButtonKind.ghost => ShadButtonVariant.outline, }; - final colors = switch (kind) { + final palette = switch (kind) { AppButtonKind.primary => (null, null, null), AppButtonKind.dark => ( - YantingColors.foreground, - YantingColors.background, - YantingColors.foreground.withValues(alpha: 0.9), + colors.foreground, + colors.background, + colors.foreground.withValues(alpha: 0.9), ), AppButtonKind.accent => ( - YantingColors.brandSoft, - YantingColors.primaryForeground, - YantingColors.brandSoftBorder, + colors.brandSoft, + colors.primaryForeground, + colors.brandSoftBorder, ), AppButtonKind.ghost => (null, null, null), }; @@ -51,13 +52,13 @@ class AppButton extends StatelessWidget { width: expand ? double.infinity : null, height: compact ? 36 : 44, padding: EdgeInsets.symmetric(horizontal: compact ? 16 : 20), - backgroundColor: colors.$1, - foregroundColor: colors.$2, - hoverBackgroundColor: colors.$3, + backgroundColor: palette.$1, + foregroundColor: palette.$2, + hoverBackgroundColor: palette.$3, leading: icon == null ? null : Icon(icon, size: compact ? 15 : 16), gap: compact ? 5 : 7, textStyle: (compact ? YantingText.badge : YantingText.body).copyWith( - color: colors.$2, + color: palette.$2, fontWeight: FontWeight.w600, ), child: Text(label), @@ -81,6 +82,7 @@ class AppIconButton extends StatelessWidget { @override Widget build(BuildContext context) { final iconWidget = Icon(icon, size: 16); + final colors = ShadTheme.of(context).colorScheme; return switch (kind) { AppButtonKind.primary => ShadIconButton( onPressed: onPressed, @@ -88,16 +90,16 @@ class AppIconButton extends StatelessWidget { ), AppButtonKind.dark => ShadIconButton( onPressed: onPressed, - backgroundColor: YantingColors.foreground, - foregroundColor: YantingColors.background, - hoverBackgroundColor: YantingColors.foreground.withValues(alpha: 0.9), + backgroundColor: colors.foreground, + foregroundColor: colors.background, + hoverBackgroundColor: colors.foreground.withValues(alpha: 0.9), icon: iconWidget, ), AppButtonKind.accent => ShadIconButton.secondary( onPressed: onPressed, - backgroundColor: YantingColors.brandSoft, - foregroundColor: YantingColors.primaryForeground, - hoverBackgroundColor: YantingColors.brandSoftBorder, + backgroundColor: colors.brandSoft, + foregroundColor: colors.primaryForeground, + hoverBackgroundColor: colors.brandSoftBorder, icon: iconWidget, ), AppButtonKind.ghost => ShadIconButton.outline( diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index ab274e4..612b146 100644 --- a/lib/widgets/app_card.dart +++ b/lib/widgets/app_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../theme/yanting_tokens.dart'; @@ -20,11 +21,17 @@ class AppCard extends StatelessWidget { @override Widget build(BuildContext context) { - final radius = BorderRadius.circular(YantingRadius.xl); + final theme = ShadTheme.of(context); + final colors = theme.colorScheme; + final radius = theme.radius.resolve(TextDirection.ltr); final decoration = BoxDecoration( - color: color, + color: color == YantingColors.card ? colors.card : color, borderRadius: radius, - border: Border.all(color: borderColor), + border: Border.all( + color: borderColor == YantingColors.border + ? colors.border + : borderColor, + ), ); if (onTap == null) { return DecoratedBox( @@ -40,8 +47,8 @@ class AppCard extends StatelessWidget { decoration: decoration, child: InkWell( borderRadius: radius, - splashColor: YantingColors.mutedForeground.withValues(alpha: 0.08), - highlightColor: YantingColors.mutedForeground.withValues(alpha: 0.04), + splashColor: colors.mutedForeground.withValues(alpha: 0.08), + highlightColor: colors.mutedForeground.withValues(alpha: 0.04), onTap: onTap, child: Padding(padding: padding, child: child), ), @@ -58,10 +65,11 @@ class HeroReportCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return AppCard( onTap: onTap, - color: YantingColors.brandSoft, - borderColor: YantingColors.brandSoftBorder, + color: colors.brandSoft, + borderColor: colors.brandSoftBorder, padding: const EdgeInsets.all(YantingSpacing.cardPadding), child: child, ); diff --git a/lib/widgets/badges.dart b/lib/widgets/badges.dart index 8f18e44..e3a747d 100644 --- a/lib/widgets/badges.dart +++ b/lib/widgets/badges.dart @@ -17,6 +17,7 @@ class AppBadge extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final child = Row( mainAxisSize: MainAxisSize.min, children: [ @@ -27,7 +28,7 @@ class AppBadge extends StatelessWidget { final shape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(YantingRadius.sm), side: kind == BadgeKind.tier || kind == BadgeKind.warning - ? const BorderSide(color: YantingColors.border) + ? BorderSide(color: colors.border) : BorderSide.none, ); @@ -45,14 +46,15 @@ class AppBadge extends StatelessWidget { BadgeKind.tier => ShadBadge.outline( shape: shape, padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), - foregroundColor: YantingColors.mutedForeground, + foregroundColor: colors.mutedForeground, child: child, ), BadgeKind.warning => ShadBadge.destructive( shape: shape, padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), - backgroundColor: YantingColors.background, - foregroundColor: YantingColors.destructive, + backgroundColor: colors.warningSoft, + foregroundColor: colors.warningSoftForeground, + hoverBackgroundColor: colors.warningSoftBorder, child: child, ), }; @@ -75,19 +77,18 @@ class AppChip extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return ShadBadge.secondary( onPressed: onTap, shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 9), - backgroundColor: selected - ? YantingColors.foreground - : YantingColors.secondary, + backgroundColor: selected ? colors.foreground : colors.secondary, hoverBackgroundColor: selected - ? YantingColors.foreground.withValues(alpha: 0.9) - : YantingColors.border, + ? colors.foreground.withValues(alpha: 0.9) + : colors.border, foregroundColor: selected - ? YantingColors.background - : YantingColors.secondaryForeground, + ? colors.background + : colors.secondaryForeground, child: Text(label), ); } diff --git a/lib/widgets/bottom_tab_bar.dart b/lib/widgets/bottom_tab_bar.dart index 72b1518..0d09b25 100644 --- a/lib/widgets/bottom_tab_bar.dart +++ b/lib/widgets/bottom_tab_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../theme/app_icons.dart'; import '../theme/yanting_text.dart'; @@ -30,24 +31,28 @@ class BottomTabBar extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return DecoratedBox( - decoration: const BoxDecoration( - color: YantingColors.background, - border: Border(top: BorderSide(color: YantingColors.border)), - ), + decoration: const BoxDecoration(color: Colors.transparent), child: SizedBox( height: YantingSpacing.tabBarHeight, - child: Row( - children: [ - for (var index = 0; index < items.length; index++) - Expanded( - child: _BottomTabButton( - item: items[index], - selected: index == selectedIndex, - onTap: () => onSelected(index), + child: DecoratedBox( + decoration: BoxDecoration( + color: colors.background, + border: Border(top: BorderSide(color: colors.border)), + ), + child: Row( + children: [ + for (var index = 0; index < items.length; index++) + Expanded( + child: _BottomTabButton( + item: items[index], + selected: index == selectedIndex, + onTap: () => onSelected(index), + ), ), - ), - ], + ], + ), ), ), ); @@ -67,9 +72,8 @@ class _BottomTabButton extends StatelessWidget { @override Widget build(BuildContext context) { - final color = selected - ? YantingColors.foreground - : YantingColors.mutedForeground; + final colors = ShadTheme.of(context).colorScheme; + final color = selected ? colors.foreground : colors.mutedForeground; return InkWell( onTap: onTap, child: Column( diff --git a/lib/widgets/institution_card.dart b/lib/widgets/institution_card.dart index cfaaafb..7c057ba 100644 --- a/lib/widgets/institution_card.dart +++ b/lib/widgets/institution_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../data/models/models.dart'; import '../theme/yanting_text.dart'; @@ -18,6 +19,7 @@ class InstitutionCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final initials = institution.nameCn.isEmpty ? '研' : institution.nameCn.characters.take(2).toString(); @@ -43,6 +45,7 @@ class InstitutionCard extends StatelessWidget { overflow: TextOverflow.ellipsis, style: YantingText.listTitle.copyWith( fontWeight: FontWeight.w700, + color: colors.foreground, ), ), if (institution.nameEn.isNotEmpty) ...[ @@ -80,6 +83,7 @@ class InstitutionCard extends StatelessWidget { style: YantingText.sectionTitle.copyWith( fontSize: 20, fontFeatures: YantingTypographyFeatures.tabularNums, + color: colors.foreground, ), ), Text('份研报', style: YantingText.meta.copyWith(fontSize: 11)), @@ -105,17 +109,18 @@ class InstitutionLogo extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; final fallback = DecoratedBox( decoration: BoxDecoration( - color: YantingColors.secondary, - border: Border.all(color: YantingColors.border), + color: colors.secondary, + border: Border.all(color: colors.border), borderRadius: BorderRadius.circular(size * 0.25), ), child: Center( child: Text( initials, style: YantingText.meta.copyWith( - color: YantingColors.secondaryForeground, + color: colors.secondaryForeground, fontSize: 14, fontWeight: FontWeight.w700, fontFeatures: null, diff --git a/lib/widgets/mini_player.dart b/lib/widgets/mini_player.dart index d92e900..1334455 100644 --- a/lib/widgets/mini_player.dart +++ b/lib/widgets/mini_player.dart @@ -4,7 +4,6 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../data/models/models.dart'; import '../theme/app_icons.dart'; import '../theme/yanting_text.dart'; -import '../theme/yanting_tokens.dart'; import '../theme/wise_tokens.dart'; import 'app_buttons.dart'; import 'app_card.dart'; @@ -60,13 +59,14 @@ class MiniPlayer extends StatelessWidget { @override Widget build(BuildContext context) { if (!player.hasAudio) return const SizedBox.shrink(); + final colors = ShadTheme.of(context).colorScheme; final ratio = player.durationSec == 0 ? 0.0 : player.positionSec / player.durationSec; return DecoratedBox( - decoration: const BoxDecoration( - color: YantingColors.secondary, - border: Border(top: BorderSide(color: YantingColors.border)), + decoration: BoxDecoration( + color: colors.secondary, + border: Border(top: BorderSide(color: colors.border)), ), child: Stack( children: [ @@ -78,9 +78,9 @@ class MiniPlayer extends StatelessWidget { alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: ratio.clamp(0, 1), - child: const SizedBox( + child: SizedBox( height: 2, - child: ColoredBox(color: YantingColors.primary), + child: ColoredBox(color: colors.primary), ), ), ), @@ -93,12 +93,12 @@ class MiniPlayer extends StatelessWidget { width: 38, height: 38, decoration: BoxDecoration( - color: YantingColors.primary, + color: colors.primary, borderRadius: BorderRadius.circular(8), ), - child: const Icon( + child: Icon( AppIcons.disc, - color: YantingColors.primaryForeground, + color: colors.primaryForeground, size: 20, ), ), @@ -113,7 +113,7 @@ class MiniPlayer extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: YantingText.meta.copyWith( - color: YantingColors.foreground, + color: colors.foreground, fontWeight: FontWeight.w600, fontFeatures: null, ), @@ -169,9 +169,10 @@ class PlayerCard extends StatelessWidget { final active = player.hasAudio && player.title == title; final position = active ? player.positionSec : 0; final ratio = durationSec == 0 ? 0.0 : position / durationSec; + final colors = ShadTheme.of(context).colorScheme; return AppCard( - color: YantingColors.secondary, - borderColor: YantingColors.border, + color: colors.secondary, + borderColor: colors.border, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -255,17 +256,18 @@ class _SkipButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return TextButton( onPressed: onPressed, style: TextButton.styleFrom( - foregroundColor: YantingColors.foreground, + foregroundColor: colors.foreground, minimumSize: const Size(40, 40), padding: EdgeInsets.zero, ), child: Text( label, style: YantingText.meta.copyWith( - color: YantingColors.foreground, + color: colors.foreground, fontWeight: FontWeight.w600, ), ), diff --git a/lib/widgets/page_header.dart b/lib/widgets/page_header.dart index 60b0900..8e31e91 100644 --- a/lib/widgets/page_header.dart +++ b/lib/widgets/page_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../theme/yanting_text.dart'; import '../theme/yanting_tokens.dart'; @@ -11,6 +12,7 @@ class PageHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(top: 4, bottom: 18), child: Column( @@ -21,9 +23,7 @@ class PageHeader extends StatelessWidget { const SizedBox(height: 8), Text( subtitle!, - style: YantingText.sub.copyWith( - color: YantingColors.mutedForeground, - ), + style: YantingText.sub.copyWith(color: colors.mutedForeground), ), ], ], @@ -40,6 +40,7 @@ class SectionTitle extends StatelessWidget { @override Widget build(BuildContext context) { + final colors = ShadTheme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only( top: YantingSpacing.sectionGap, @@ -50,7 +51,7 @@ class SectionTitle extends StatelessWidget { Text(title, style: YantingText.sectionTitle), if (icon != null) ...[ const SizedBox(width: 6), - Icon(icon, size: 18, color: YantingColors.mutedForeground), + Icon(icon, size: 18, color: colors.mutedForeground), ], ], ), diff --git a/pubspec.lock b/pubspec.lock index 2112885..476bd4b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -461,6 +469,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.53.6" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 582e58d..dcea373 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: google_fonts: ^6.2.1 phosphor_flutter: ^2.1.0 remixicon: ^4.9.3 + shared_preferences: ^2.3.3 shadcn_ui: ^0.53.6 dev_dependencies: