diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/app/app.dart b/lib/app/app.dart index 6e31942..f58f14a 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../routing/app_router.dart'; import '../theme/app_theme.dart'; +import '../theme/yanting_text.dart'; +import '../theme/yanting_shad_theme.dart'; class ReportNotebooklmApp extends ConsumerWidget { const ReportNotebooklmApp({super.key}); @@ -10,125 +14,24 @@ class ReportNotebooklmApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); - return MaterialApp.router( + final dmSansStyle = GoogleFonts.dmSans().copyWith( + fontFamilyFallback: YantingText.fontFallback, + ); + + return ShadApp.router( title: '研听', debugShowCheckedModeBanner: false, - theme: buildAppTheme(), - scrollBehavior: const WhitespaceStretchScrollBehavior(), + theme: buildYantingShadTheme(), + darkTheme: buildYantingDarkShadTheme(), routerConfig: router, - ); - } -} - -class WhitespaceStretchScrollBehavior extends MaterialScrollBehavior { - const WhitespaceStretchScrollBehavior(); - - @override - Widget buildOverscrollIndicator( - BuildContext context, - Widget child, - ScrollableDetails details, - ) { - return _WhitespaceStretchIndicator(child: child); - } -} - -class _WhitespaceStretchIndicator extends StatefulWidget { - const _WhitespaceStretchIndicator({required this.child}); - - final Widget child; - - @override - State<_WhitespaceStretchIndicator> createState() => - _WhitespaceStretchIndicatorState(); -} - -class _WhitespaceStretchIndicatorState - extends State<_WhitespaceStretchIndicator> - with SingleTickerProviderStateMixin { - static const double _maxStretch = 64; - static const double _dragResistance = 0.38; - - late final AnimationController _offsetController = - AnimationController.unbounded(vsync: this)..addListener(_onTick); - - @override - Widget build(BuildContext context) { - return NotificationListener( - onNotification: _handleScrollNotification, - child: ClipRect( - child: Transform.translate( - offset: Offset(0, _offsetController.value), - child: widget.child, - ), - ), - ); - } - - @override - void dispose() { - _offsetController.dispose(); - super.dispose(); - } - - bool _handleScrollNotification(ScrollNotification notification) { - if (notification.metrics.axis != Axis.vertical) { - return false; - } - if (notification is OverscrollNotification) { - final overscroll = notification.overscroll; - final atTop = - notification.metrics.pixels <= notification.metrics.minScrollExtent; - final atBottom = - notification.metrics.pixels >= notification.metrics.maxScrollExtent; - if (atTop && overscroll < 0) { - _setOffset( - (_offsetController.value - overscroll * _dragResistance).clamp( - 0, - _maxStretch, - ), + scrollBehavior: const ShadScrollBehavior(), + materialThemeBuilder: (context, theme) => buildAppTheme(), + builder: (context, child) { + return DefaultTextStyle.merge( + style: TextStyle(fontFamilyFallback: dmSansStyle.fontFamilyFallback), + child: child ?? const SizedBox.shrink(), ); - } else if (atBottom && overscroll > 0) { - _setOffset( - (_offsetController.value - overscroll * _dragResistance).clamp( - -_maxStretch, - 0, - ), - ); - } - } - if (notification is ScrollUpdateNotification && - notification.dragDetails == null) { - _releaseOffset(); - } - if (notification is ScrollEndNotification) { - _releaseOffset(); - } - return false; - } - - void _setOffset(num next) { - if (next == _offsetController.value) { - return; - } - _offsetController.stop(); - _offsetController.value = next.toDouble(); - } - - void _releaseOffset() { - if (_offsetController.value == 0) { - return; - } - _offsetController.animateTo( - 0, - duration: const Duration(milliseconds: 260), - curve: Curves.easeOutCubic, + }, ); } - - void _onTick() { - if (mounted) { - setState(() {}); - } - } } diff --git a/lib/features/detail/modules/renderer_registry.dart b/lib/features/detail/modules/renderer_registry.dart index b4feb83..838de71 100644 --- a/lib/features/detail/modules/renderer_registry.dart +++ b/lib/features/detail/modules/renderer_registry.dart @@ -1,16 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../data/api/report_data_source.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'; import '../../../widgets/badges.dart'; import '../../../widgets/mini_player.dart'; +import '../../../widgets/states.dart'; typedef StartModuleAudio = void Function( @@ -53,7 +55,7 @@ class ModuleRendererRegistry { crossAxisAlignment: CrossAxisAlignment.start, children: [ _ModuleHeader(module: module), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), _contentFor( context, type: module.type, @@ -69,13 +71,14 @@ class ModuleRendererRegistry { compact: module.renderMode != 'inline', ), if (module.hasDetailPage) ...[ - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Align( alignment: Alignment.centerLeft, - child: TextButton.icon( + child: AppButton( onPressed: openDetail, - icon: const Icon(AppIcons.externalLink), - label: const Text('查看详情'), + icon: AppIcons.externalLink, + kind: AppButtonKind.ghost, + label: '查看详情', ), ), ], @@ -180,19 +183,31 @@ class ModuleDetailPage extends HookConsumerWidget { [dataSource, reportId, module.id, retryCount.value], ); final snapshot = useFuture(future); + final theme = ShadTheme.of(context); return Scaffold( - appBar: AppBar(title: Text(module.titleCn)), + backgroundColor: theme.colorScheme.background, + appBar: AppBar( + backgroundColor: theme.colorScheme.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + title: Text(module.titleCn), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: ColoredBox( + color: theme.colorScheme.border, + child: const SizedBox(height: 1, width: double.infinity), + ), + ), + ), body: snapshot.connectionState != ConnectionState.done - ? const Center(child: CircularProgressIndicator()) + ? const LoadingState() : snapshot.hasError ? Center( - child: TextButton( + child: AppButton( onPressed: () => retryCount.value++, - child: Text( - snapshot.error.toString(), - textAlign: TextAlign.center, - ), + kind: AppButtonKind.ghost, + label: snapshot.error.toString(), ), ) : _ModuleDetailContent( @@ -218,13 +233,18 @@ class _ModuleDetailContent extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16), + padding: const EdgeInsets.fromLTRB( + YantingSpacing.x4, + 4, + YantingSpacing.x4, + 16, + ), children: [ Text( detail.titleCn, style: YantingText.sectionTitle.copyWith(fontSize: 21), ), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), AppCard( child: registry.page( context, @@ -233,7 +253,7 @@ class _ModuleDetailContent extends StatelessWidget { report: report, ), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Text('缓存版本 ${detail.cacheVersion}', style: YantingText.meta), ], ); @@ -283,10 +303,10 @@ class _BasicInfo extends StatelessWidget { ), style: YantingText.body, ), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), Wrap( - spacing: WiseSpacing.x2, - runSpacing: WiseSpacing.x2, + spacing: YantingSpacing.x2, + runSpacing: YantingSpacing.x2, children: [ for (final topic in topics) AppBadge(text: topic), if (report?.releasedAt != null) @@ -312,8 +332,8 @@ class _CoreInsights extends StatelessWidget { children: [ for (final point in points) Container( - margin: const EdgeInsets.only(bottom: WiseSpacing.x3), - padding: const EdgeInsets.all(WiseSpacing.x3), + margin: const EdgeInsets.only(bottom: YantingSpacing.x3), + padding: const EdgeInsets.all(YantingSpacing.x3), decoration: BoxDecoration( color: YantingColors.background, border: Border.all(color: YantingColors.border), @@ -326,7 +346,7 @@ class _CoreInsights extends StatelessWidget { text: _kindLabel(asString(point['kind'])), kind: _kindBadge(asString(point['kind'])), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text(asString(point['text']), style: YantingText.body), ], ), @@ -351,9 +371,9 @@ class _SourceCompliance extends StatelessWidget { if (asString(payload['source_note']).isNotEmpty) Text(asString(payload['source_note']), style: YantingText.body), if (institution != null) ...[ - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), Text('发布机构', style: YantingText.cardTitle.copyWith(fontSize: 17)), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), _InfoLine(label: '机构名称', value: institution.nameCn), if (institution.nameEn.isNotEmpty) _InfoLine(label: '英文名称', value: institution.nameEn), @@ -377,10 +397,10 @@ class _SourceCompliance extends StatelessWidget { _InfoLine(label: '说明', value: institution.introCn), ], if (asString(payload['copyright_cn']).isNotEmpty) ...[ - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), Text(asString(payload['copyright_cn']), style: YantingText.meta), ], - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), DecoratedBox( decoration: BoxDecoration( color: YantingColors.background, @@ -388,7 +408,7 @@ class _SourceCompliance extends StatelessWidget { borderRadius: BorderRadius.circular(YantingRadius.md), ), child: Padding( - padding: const EdgeInsets.all(WiseSpacing.x3), + padding: const EdgeInsets.all(YantingSpacing.x3), child: Text( asString(payload['disclaimer'], '本内容为公开/授权研报的结构化解读,不构成投资建议。'), style: YantingText.meta.copyWith(color: YantingColors.warning), @@ -410,7 +430,7 @@ class _InfoLine extends StatelessWidget { Widget build(BuildContext context) { if (value.isEmpty) return const SizedBox.shrink(); return Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x2), + padding: const EdgeInsets.only(bottom: YantingSpacing.x2), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -420,7 +440,7 @@ class _InfoLine extends StatelessWidget { color: YantingColors.mutedForeground, ), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text(value, style: YantingText.body), ], ), @@ -480,7 +500,7 @@ class _InstitutionModule extends StatelessWidget { return Row( children: [ const Icon(AppIcons.bank, color: YantingColors.foreground), - const SizedBox(width: WiseSpacing.x2), + const SizedBox(width: YantingSpacing.x2), Expanded(child: Text(name, style: YantingText.body)), Text( '${asInt(payload['report_count'], report?.institution.reportCount ?? 0)} 份', @@ -510,12 +530,12 @@ class _SectionsModule extends StatelessWidget { children: [ if (summary.isNotEmpty) Text(summary, style: YantingText.body), for (final section in sections) ...[ - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Text( asString(section['heading']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text(asString(section['body']), style: YantingText.body), ], ], @@ -538,8 +558,8 @@ class _KeyDataModule extends StatelessWidget { children: [ for (final row in rows) Container( - margin: const EdgeInsets.only(bottom: WiseSpacing.x3), - padding: const EdgeInsets.all(WiseSpacing.x3), + margin: const EdgeInsets.only(bottom: YantingSpacing.x3), + padding: const EdgeInsets.all(YantingSpacing.x3), decoration: BoxDecoration( color: YantingColors.secondary, borderRadius: BorderRadius.circular(YantingRadius.md), @@ -562,7 +582,7 @@ class _KeyDataModule extends StatelessWidget { ], ), ), - const SizedBox(width: WiseSpacing.x2), + const SizedBox(width: YantingSpacing.x2), Text( _valueWithUnit(row), textAlign: TextAlign.right, @@ -635,10 +655,10 @@ class _TimelineEntry extends StatelessWidget { ), ], ), - const SizedBox(width: WiseSpacing.x2), + const SizedBox(width: YantingSpacing.x2), Expanded( child: Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x3), + padding: const EdgeInsets.only(bottom: YantingSpacing.x3), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -650,12 +670,12 @@ class _TimelineEntry extends StatelessWidget { fontWeight: FontWeight.w600, ), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text( asString(event['event']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text(asString(event['impact']), style: YantingText.body), ], ), @@ -695,10 +715,10 @@ class _StudyGuideModule extends StatelessWidget { ], ), if (glossary.isNotEmpty) ...[ - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Wrap( - spacing: WiseSpacing.x2, - runSpacing: WiseSpacing.x2, + spacing: YantingSpacing.x2, + runSpacing: YantingSpacing.x2, children: [ for (final item in glossary) AppBadge( @@ -730,10 +750,10 @@ class _StructureGraphModule extends StatelessWidget { asString(payload['root']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), for (final node in nodes) Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x4), + padding: const EdgeInsets.only(bottom: YantingSpacing.x4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -741,10 +761,10 @@ class _StructureGraphModule extends StatelessWidget { asString(node['label']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), for (final child in asStringList(node['children'])) Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x1), + padding: const EdgeInsets.only(bottom: YantingSpacing.x1), child: Text(child, style: YantingText.body), ), ], @@ -770,7 +790,7 @@ class _RelatedSourcesModule extends StatelessWidget { children: [ for (final item in items) Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x4), + padding: const EdgeInsets.only(bottom: YantingSpacing.x4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -778,7 +798,7 @@ class _RelatedSourcesModule extends StatelessWidget { asString(item['title']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text( asString(item['summary_cn'], asString(item['source_name'])), style: YantingText.body, @@ -809,7 +829,7 @@ class _DifferentiatedViewModule extends StatelessWidget { children: [ for (final item in items) Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x4), + padding: const EdgeInsets.only(bottom: YantingSpacing.x4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -817,7 +837,7 @@ class _DifferentiatedViewModule extends StatelessWidget { asString(item['topic']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), if (asString(item['consensus_view']).isNotEmpty) ...[ Text( '常见观点', @@ -825,12 +845,12 @@ class _DifferentiatedViewModule extends StatelessWidget { color: YantingColors.mutedForeground, ), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text( asString(item['consensus_view']), style: YantingText.body, ), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), ], if (asString(item['report_position']).isNotEmpty) ...[ Text( @@ -839,7 +859,7 @@ class _DifferentiatedViewModule extends StatelessWidget { color: YantingColors.foreground, ), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text( asString(item['report_position']), style: YantingText.body, @@ -877,8 +897,8 @@ class _WeaknessesModule extends StatelessWidget { for (final item in items) Padding( padding: const EdgeInsets.only( - top: WiseSpacing.x3, - bottom: WiseSpacing.x2, + top: YantingSpacing.x3, + bottom: YantingSpacing.x2, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -887,20 +907,20 @@ class _WeaknessesModule extends StatelessWidget { asString(item['topic']), style: YantingText.cardTitle.copyWith(fontSize: 17), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), Text(asString(item['weakness']), style: YantingText.body), ], ), ), if (verificationNotes.isNotEmpty || counterEvidence.isNotEmpty) ...[ - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), DecoratedBox( decoration: BoxDecoration( color: const Color(0x109A6500), - borderRadius: BorderRadius.circular(WiseRadius.sm), + borderRadius: BorderRadius.circular(YantingRadius.sm), ), child: Padding( - padding: const EdgeInsets.all(WiseSpacing.x3), + padding: const EdgeInsets.all(YantingSpacing.x3), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -910,13 +930,13 @@ class _WeaknessesModule extends StatelessWidget { color: YantingColors.warning, ), ), - const SizedBox(height: WiseSpacing.x1), + const SizedBox(height: YantingSpacing.x1), for (final note in verificationNotes.isNotEmpty ? verificationNotes : counterEvidence) Padding( - padding: const EdgeInsets.only(bottom: WiseSpacing.x1), + padding: const EdgeInsets.only(bottom: YantingSpacing.x1), child: Text(note, style: YantingText.meta), ), ], @@ -947,7 +967,7 @@ class _Preview extends StatelessWidget { if (headline.isNotEmpty) Text(headline, style: YantingText.body), for (final item in highlights.take(3)) Padding( - padding: const EdgeInsets.only(top: WiseSpacing.x1), + padding: const EdgeInsets.only(top: YantingSpacing.x1), child: Text('• $item', style: YantingText.meta), ), ], @@ -988,7 +1008,7 @@ class _FallbackModule extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AppBadge(text: '未知模块:$type', kind: BadgeKind.warning), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), _Preview(payload: payload), ], ); diff --git a/lib/features/detail/report_detail_page.dart b/lib/features/detail/report_detail_page.dart index 8c7d949..748ba15 100644 --- a/lib/features/detail/report_detail_page.dart +++ b/lib/features/detail/report_detail_page.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.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'; import '../../widgets/badges.dart'; @@ -52,9 +52,23 @@ class ReportDetailPage extends HookConsumerWidget { ]); final snapshot = useFuture(detailFuture); const registry = ModuleRendererRegistry(); + final theme = ShadTheme.of(context); return Scaffold( - appBar: AppBar(title: const Text('研报详情')), + backgroundColor: theme.colorScheme.background, + appBar: AppBar( + backgroundColor: theme.colorScheme.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + title: const Text('研报详情'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: ColoredBox( + color: theme.colorScheme.border, + child: const SizedBox(height: 1, width: double.infinity), + ), + ), + ), body: snapshot.connectionState != ConnectionState.done ? const LoadingState() : snapshot.hasError @@ -106,7 +120,12 @@ class _ReportDetailContent extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16), + padding: const EdgeInsets.fromLTRB( + YantingSpacing.x4, + 4, + YantingSpacing.x4, + 16, + ), children: [ AppCard( color: YantingColors.brandSoft, @@ -115,8 +134,8 @@ class _ReportDetailContent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( - spacing: WiseSpacing.x2, - runSpacing: WiseSpacing.x2, + spacing: YantingSpacing.x2, + runSpacing: YantingSpacing.x2, children: [ AppBadge( text: detail.interpretationLabel, @@ -134,7 +153,7 @@ class _ReportDetailContent extends StatelessWidget { ), ], ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Text( detail.titleCn, maxLines: 3, @@ -142,10 +161,10 @@ class _ReportDetailContent extends StatelessWidget { style: YantingText.sectionTitle.copyWith(fontSize: 21), ), if (detail.oneLiner.isNotEmpty) ...[ - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), Text(detail.oneLiner, style: YantingText.body), ], - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), Text( '${detail.institution.nameCn} · ${formatDate(detail.releasedAt)}', style: YantingText.meta, @@ -153,11 +172,11 @@ class _ReportDetailContent extends StatelessWidget { ], ), ), - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), _ActionBar(detail: detail), - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), _Toc(modules: detail.modules), - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), for (final module in detail.modules) ...[ registry.card( context: context, @@ -170,7 +189,7 @@ class _ReportDetailContent extends StatelessWidget { onSeekAudio: onSeekAudio, onSpeed: onSpeed, ), - const SizedBox(height: WiseSpacing.x4), + const SizedBox(height: YantingSpacing.x4), ], ], ); @@ -194,7 +213,7 @@ class _ActionBar extends StatelessWidget { onPressed: () => showLoginSheet(context, reason: '登录后保存到你的收藏'), ), ), - const SizedBox(width: WiseSpacing.x2), + const SizedBox(width: YantingSpacing.x2), Expanded( child: AppButton( label: '原文', @@ -222,7 +241,7 @@ class _Toc extends StatelessWidget { children: [ for (final module in modules) Padding( - padding: const EdgeInsets.only(right: WiseSpacing.x2), + padding: const EdgeInsets.only(right: YantingSpacing.x2), child: AppBadge(text: module.titleCn, kind: BadgeKind.brand), ), ], diff --git a/lib/features/feed/feed_page.dart b/lib/features/feed/feed_page.dart index 8372432..a6fc0de 100644 --- a/lib/features/feed/feed_page.dart +++ b/lib/features/feed/feed_page.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; import '../../data/models/models.dart'; import '../../routing/app_routes.dart'; -import '../../theme/wise_tokens.dart'; +import '../../theme/yanting_tokens.dart'; import '../../widgets/badges.dart'; import '../../widgets/mini_player.dart'; import '../../widgets/page_header.dart'; @@ -66,9 +67,9 @@ class FeedPage extends HookConsumerWidget { } return ListView( padding: const EdgeInsets.fromLTRB( - WiseSpacing.x4, + YantingSpacing.screenX, 4, - WiseSpacing.x4, + YantingSpacing.screenX, 16, ), children: [ @@ -79,7 +80,7 @@ class FeedPage extends HookConsumerWidget { children: [ for (final t in topics) Padding( - padding: const EdgeInsets.only(right: WiseSpacing.x2), + padding: const EdgeInsets.only(right: YantingSpacing.x2), child: AppChip( label: t, selected: t == currentTopic, @@ -89,7 +90,9 @@ class FeedPage extends HookConsumerWidget { ], ), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), + const ShadSeparator.horizontal(), + const SizedBox(height: YantingSpacing.x3), if (visible.isEmpty) const EmptyState( title: '暂无可推荐的研报解读', @@ -112,7 +115,7 @@ class FeedPage extends HookConsumerWidget { ), onPlayTap: () => _playFromReport(onPlay, visible.first), ), - const SizedBox(height: WiseSpacing.x5), + const SizedBox(height: YantingSpacing.x6), const SectionTitle(title: '最新解读', icon: Icons.chevron_right), for (final report in visible.skip(1)) ...[ ReportCardWidget( @@ -129,7 +132,7 @@ class FeedPage extends HookConsumerWidget { ), onPlayTap: () => _playFromReport(onPlay, report), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), ], ], ], diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart new file mode 100644 index 0000000..5fafe4c --- /dev/null +++ b/lib/features/home/home_page.dart @@ -0,0 +1,221 @@ +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 '../../data/api/report_data_source.dart'; +import '../../data/content_providers.dart'; +import '../../data/models/models.dart'; +import '../../routing/app_routes.dart'; +import '../../theme/app_icons.dart'; +import '../../theme/yanting_text.dart'; +import '../../theme/yanting_tokens.dart'; +import '../../widgets/app_card.dart'; +import '../../widgets/badges.dart'; +import '../../widgets/mini_player.dart'; +import '../../widgets/page_header.dart'; +import '../../widgets/states.dart'; +import '../shared/report_card_widget.dart'; + +class HomePage extends HookConsumerWidget { + const HomePage({ + required this.dataSource, + required this.onPlay, + this.player = const PlayerStateModel(), + this.onStartModuleAudio, + this.onToggleAudio, + this.onSeekAudio, + this.onSpeed, + super.key, + }); + + final ReportDataSource dataSource; + final void Function(AudioItem item) onPlay; + final PlayerStateModel player; + final void Function( + String audioId, + String reportId, + String title, + int durationSec, + )? + onStartModuleAudio; + final VoidCallback? onToggleAudio; + final void Function(int delta)? onSeekAudio; + final VoidCallback? onSpeed; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final snapshot = ref.watch(recommendedReportsProvider); + + return snapshot.when( + loading: () => const LoadingState(), + error: (error, _) => ErrorState( + message: error.toString(), + onRetry: () => ref.invalidate(recommendedReportsProvider), + ), + data: (items) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + YantingSpacing.screenX, + 4, + YantingSpacing.screenX, + 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const PageHeader(title: '研听', subtitle: '全球机构研报中文解读'), + const SectionTitle(title: '推荐'), + if (items.isEmpty) + const EmptyState(title: '暂无可推荐的研报解读', message: '稍后再来看看最新内容') + else + ReportCardWidget( + report: items.first, + hero: true, + onTap: () => openReportDetail( + context, + dataSource, + items.first, + player: player, + onStartAudio: onStartModuleAudio, + onToggleAudio: onToggleAudio, + onSeekAudio: onSeekAudio, + onSpeed: onSpeed, + ), + onPlayTap: () => _playFromReport(onPlay, items.first), + ), + const SizedBox(height: YantingSpacing.x6), + for (final item in _directoryItems) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _DirectoryCard(item: item), + ), + ], + ), + ); + }, + ); + } +} + +class _DirectoryItem { + const _DirectoryItem({ + required this.title, + required this.subtitle, + required this.icon, + required this.path, + }); + + final String title; + final String subtitle; + final IconData icon; + final String path; +} + +class _DirectoryCard extends StatelessWidget { + const _DirectoryCard({required this.item}); + + final _DirectoryItem item; + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + + return AppCard( + onTap: () => context.push(item.path), + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + item.icon, + size: 20, + color: theme.colorScheme.secondaryForeground, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text(item.title, style: YantingText.listTitle), + ), + if (item.title == '推荐') ...[ + const SizedBox(width: 8), + const AppBadge(text: '首页', kind: BadgeKind.tier), + ], + ], + ), + const SizedBox(height: 2), + Text(item.subtitle, style: YantingText.meta), + ], + ), + ), + Icon( + LucideIcons.chevronRight, + size: 16, + color: theme.colorScheme.mutedForeground, + ), + ], + ), + ); + } +} + +const _directoryItems = [ + _DirectoryItem( + title: '推荐', + subtitle: '主题筛选后的重点研报解读', + icon: AppIcons.sparkle, + path: AppRoutes.home, + ), + _DirectoryItem( + title: '研报', + subtitle: '搜索、筛选和浏览全部研报', + icon: AppIcons.article, + path: AppRoutes.reports, + ), + _DirectoryItem( + title: '机构', + subtitle: '按机构查看来源与覆盖主题', + icon: AppIcons.bank, + path: AppRoutes.institutions, + ), + _DirectoryItem( + title: '听单', + subtitle: '继续收听音频解读', + icon: AppIcons.headphones, + path: AppRoutes.listen, + ), + _DirectoryItem( + title: '我的', + subtitle: '登录、收藏与合规说明', + icon: AppIcons.user, + path: AppRoutes.profile, + ), +]; + +void _playFromReport( + void Function(AudioItem item) onPlay, + ReportCardModel report, +) { + onPlay( + AudioItem( + audioId: 'local_${report.id}', + reportId: report.id, + titleCn: report.titleCn, + reportTitleCn: report.titleCn, + durationSec: 180, + institution: report.institution, + ), + ); +} diff --git a/lib/features/institutions/institution_detail_page.dart b/lib/features/institutions/institution_detail_page.dart index 0ea8785..cf66c76 100644 --- a/lib/features/institutions/institution_detail_page.dart +++ b/lib/features/institutions/institution_detail_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/models/models.dart'; @@ -8,7 +9,6 @@ import '../../routing/app_routes.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'; import '../../widgets/badges.dart'; @@ -35,9 +35,23 @@ class InstitutionDetailPage extends HookConsumerWidget { [dataSource, institutionId, retryCount.value], ); final snapshot = useFuture(future); + final theme = ShadTheme.of(context); return Scaffold( - appBar: AppBar(title: const Text('机构主页')), + backgroundColor: theme.colorScheme.background, + appBar: AppBar( + backgroundColor: theme.colorScheme.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + title: const Text('机构主页'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: ColoredBox( + color: theme.colorScheme.border, + child: const SizedBox(height: 1, width: double.infinity), + ), + ), + ), body: snapshot.connectionState != ConnectionState.done ? const LoadingState() : snapshot.hasError @@ -65,7 +79,12 @@ class _InstitutionDetailContent extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16), + padding: const EdgeInsets.fromLTRB( + YantingSpacing.x4, + 4, + YantingSpacing.x4, + 16, + ), children: [ AppCard( color: YantingColors.brandSoft, @@ -116,26 +135,26 @@ class _InstitutionDetailContent extends StatelessWidget { ], ), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), if (item.introCn.isNotEmpty) AppCard(child: Text(item.introCn, style: YantingText.body)), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), if (item.credibilityNote.isNotEmpty) AppCard( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon(AppIcons.shield, color: YantingColors.chart2), - const SizedBox(width: WiseSpacing.x2), + const SizedBox(width: YantingSpacing.x2), Expanded( child: Text(item.credibilityNote, style: YantingText.body), ), ], ), ), - const SizedBox(height: WiseSpacing.x5), + const SizedBox(height: YantingSpacing.x6), Text('最新研报', style: YantingText.sectionTitle.copyWith(fontSize: 21)), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), if (item.recentReports.isEmpty) const EmptyState( title: '机构暂无研报', @@ -148,7 +167,7 @@ class _InstitutionDetailContent extends StatelessWidget { report: report, onTap: () => openReportDetail(context, dataSource, report), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), ], AppButton( label: '了解相关服务', diff --git a/lib/features/institutions/institutions_page.dart b/lib/features/institutions/institutions_page.dart index 23e511b..9a5b97c 100644 --- a/lib/features/institutions/institutions_page.dart +++ b/lib/features/institutions/institutions_page.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; import '../../routing/app_routes.dart'; -import '../../theme/wise_tokens.dart'; +import '../../theme/yanting_tokens.dart'; import '../../widgets/institution_card.dart'; import '../../widgets/page_header.dart'; import '../../widgets/states.dart'; @@ -35,20 +36,23 @@ class InstitutionsPage extends HookConsumerWidget { } return ListView( padding: const EdgeInsets.fromLTRB( - WiseSpacing.x4, + YantingSpacing.screenX, 4, - WiseSpacing.x4, + YantingSpacing.screenX, 16, ), children: [ const PageHeader(title: '机构', subtitle: '可获取研报的机构'), + const SizedBox(height: YantingSpacing.x3), + const ShadSeparator.horizontal(), + const SizedBox(height: YantingSpacing.x3), for (final item in sorted) ...[ InstitutionCard( institution: item, onTap: () => openInstitutionDetail(context, dataSource, item.id), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), ], ], ); diff --git a/lib/features/listen/listen_page.dart b/lib/features/listen/listen_page.dart index 8493f26..9cbf472 100644 --- a/lib/features/listen/listen_page.dart +++ b/lib/features/listen/listen_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; @@ -7,7 +8,6 @@ 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_card.dart'; import '../../widgets/badges.dart'; import '../../widgets/page_header.dart'; @@ -39,9 +39,9 @@ class ListenPage extends HookConsumerWidget { final current = items.first; return ListView( padding: const EdgeInsets.fromLTRB( - WiseSpacing.x4, + YantingSpacing.screenX, 4, - WiseSpacing.x4, + YantingSpacing.screenX, 16, ), children: [ @@ -52,9 +52,11 @@ class ListenPage extends HookConsumerWidget { onPlay: () => onPlay(current), ), const SectionTitle(title: '全部音频', icon: AppIcons.arrowRight), + const ShadSeparator.horizontal(), + const SizedBox(height: YantingSpacing.x3), for (final item in items.skip(1)) ...[ _AudioListCard(item: item, onPlay: () => onPlay(item)), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), ], ], ); @@ -113,26 +115,17 @@ class _ContinueListeningCard extends StatelessWidget { const SizedBox(height: 16), Row( children: [ - IconButton.filled( + ShadButton( onPressed: onPlay, - icon: const Icon(AppIcons.play), - style: IconButton.styleFrom( - backgroundColor: YantingColors.primary, - foregroundColor: YantingColors.primaryForeground, - fixedSize: const Size(48, 48), - ), + width: 48, + height: 48, + child: const Icon(AppIcons.play, size: 18), ), const SizedBox(width: 13), Expanded( child: Column( children: [ - LinearProgressIndicator( - value: 0.42, - minHeight: 5, - borderRadius: BorderRadius.circular(YantingRadius.pill), - backgroundColor: YantingColors.border, - color: YantingColors.primary, - ), + const ShadProgress(value: 0.42), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -204,14 +197,11 @@ class _AudioListCard extends StatelessWidget { ), ), const SizedBox(width: 10), - IconButton.filled( + ShadButton( onPressed: onPlay, - icon: const Icon(AppIcons.play), - style: IconButton.styleFrom( - backgroundColor: YantingColors.primary, - foregroundColor: YantingColors.primaryForeground, - fixedSize: const Size(44, 44), - ), + width: 44, + height: 44, + child: const Icon(AppIcons.play, size: 16), ), ], ), diff --git a/lib/features/profile/profile_page.dart b/lib/features/profile/profile_page.dart index 4bcd2a3..e3d66dc 100644 --- a/lib/features/profile/profile_page.dart +++ b/lib/features/profile/profile_page.dart @@ -4,7 +4,6 @@ import '../../data/api/report_data_source.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'; import '../../widgets/page_header.dart'; @@ -19,7 +18,12 @@ class ProfilePage extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( - padding: const EdgeInsets.fromLTRB(WiseSpacing.x4, 4, WiseSpacing.x4, 16), + padding: const EdgeInsets.fromLTRB( + YantingSpacing.screenX, + 4, + YantingSpacing.screenX, + 16, + ), children: [ const PageHeader(title: '我的'), AppCard( @@ -52,7 +56,7 @@ class ProfilePage extends StatelessWidget { ], ), ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), AppButton( label: '登录 / 注册', expand: true, @@ -69,7 +73,7 @@ class ProfilePage extends StatelessWidget { ), ], ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), _MenuGroup( children: [ _MenuRow( @@ -89,7 +93,7 @@ class ProfilePage extends StatelessWidget { ), ], ), - const SizedBox(height: WiseSpacing.x3), + const SizedBox(height: YantingSpacing.x3), AppCard( color: YantingColors.secondary, onTap: () => showOutboundSheet(context, title: '相关服务'), diff --git a/lib/features/reports/reports_page.dart b/lib/features/reports/reports_page.dart index 0312caf..84c0bfc 100644 --- a/lib/features/reports/reports_page.dart +++ b/lib/features/reports/reports_page.dart @@ -1,17 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../../data/api/report_data_source.dart'; import '../../data/content_providers.dart'; import '../../data/models/models.dart'; import '../../routing/app_routes.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/badges.dart'; import '../../widgets/mini_player.dart'; import '../../widgets/page_header.dart'; import '../../widgets/states.dart'; @@ -45,6 +42,8 @@ class ReportsPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final theme = ShadTheme.of(context); + final searchController = useTextEditingController(); final query = useState(''); final topic = useState(''); final hasAudio = useState(false); @@ -67,104 +66,127 @@ class ReportsPage extends HookConsumerWidget { hasAudio: currentHasAudio, ); - return ListView( + return SingleChildScrollView( padding: const EdgeInsets.fromLTRB( - WiseSpacing.x4, + YantingSpacing.screenX, 4, - WiseSpacing.x4, + YantingSpacing.screenX, 16, ), - children: [ - const PageHeader(title: '研报', subtitle: '全部已发布研报解读'), - TextField( - decoration: InputDecoration( - hintText: '搜索标题、机构或主题', - prefixIcon: const Icon(AppIcons.search), - suffixIcon: currentQuery.isEmpty + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const PageHeader(title: '研报', subtitle: '全部已发布研报解读'), + ShadInput( + controller: searchController, + placeholder: const Text('搜索标题、机构或主题'), + leading: const Padding( + padding: EdgeInsets.only(right: 8), + child: Icon(LucideIcons.search, size: 16), + ), + trailing: currentQuery.isEmpty ? null - : IconButton( - onPressed: () => query.value = '', - icon: const Icon(Icons.close), - ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(YantingRadius.md), - borderSide: const BorderSide(color: YantingColors.input), - ), - ), - onChanged: (value) => query.value = value.trim(), - ), - const SizedBox(height: WiseSpacing.x3), - Row( - children: [ - AppButton( - label: '筛选', - icon: AppIcons.filter, - kind: AppButtonKind.ghost, - onPressed: items.isEmpty - ? null - : () => _openFilterSheet( - context, - items: items, - topic: topic, + : Padding( + padding: const EdgeInsets.only(left: 8), + child: ShadButton.ghost( + size: ShadButtonSize.sm, + onPressed: () { + searchController.clear(); + query.value = ''; + }, + child: const Icon(LucideIcons.x, size: 16), ), - ), - const SizedBox(width: WiseSpacing.x2), - AppButton( - label: '最新', - icon: AppIcons.sort, - kind: AppButtonKind.ghost, - onPressed: () {}, - ), - const SizedBox(width: WiseSpacing.x2), - AppChip( - label: '音频', - selected: currentHasAudio, - onTap: () => hasAudio.value = !currentHasAudio, - ), - const Spacer(), - Text('共 ${filtered.length} 篇', style: YantingText.meta), - ], - ), - const SizedBox(height: WiseSpacing.x3), - if (filtered.isEmpty) - EmptyState( - title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报', - message: currentQuery.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试', - actionLabel: '清除筛选', - onAction: () { - query.value = ''; - topic.value = ''; - hasAudio.value = false; - }, - ) - else - for (final report in filtered) ...[ - ReportCardWidget( - report: report, - onTap: () => openReportDetail( - context, - dataSource, - report, - player: player, - onStartAudio: onStartModuleAudio, - onToggleAudio: onToggleAudio, - onSeekAudio: onSeekAudio, - onSpeed: onSpeed, + ), + onChanged: (value) => query.value = value.trim(), + ), + const SizedBox(height: YantingSpacing.x3), + Wrap( + spacing: YantingSpacing.x2, + runSpacing: YantingSpacing.x2, + children: [ + ShadButton.outline( + onPressed: items.isEmpty + ? null + : () => _openFilterSheet( + context, + items: items, + topic: topic, + ), + leading: const Icon( + LucideIcons.slidersHorizontal, + size: 16, + ), + child: const Text('筛选'), ), - onPlayTap: () => onPlay( - AudioItem( - audioId: 'local_${report.id}', - reportId: report.id, - titleCn: report.titleCn, - reportTitleCn: report.titleCn, - durationSec: 180, - institution: report.institution, + ShadButton.outline( + onPressed: () {}, + leading: const Icon(LucideIcons.arrowUpDown, size: 16), + child: const Text('最新'), + ), + ShadBadge.secondary( + onPressed: () => hasAudio.value = !currentHasAudio, + backgroundColor: currentHasAudio + ? theme.colorScheme.foreground + : theme.colorScheme.secondary, + foregroundColor: currentHasAudio + ? theme.colorScheme.background + : theme.colorScheme.secondaryForeground, + hoverBackgroundColor: currentHasAudio + ? theme.colorScheme.foreground.withValues(alpha: 0.9) + : theme.colorScheme.border, + child: const Text('音频'), + ), + ], + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerRight, + child: Text('共 ${filtered.length} 篇', style: YantingText.meta), + ), + const SizedBox(height: YantingSpacing.x3), + const ShadSeparator.horizontal(), + const SizedBox(height: YantingSpacing.x3), + if (filtered.isEmpty) + EmptyState( + title: currentQuery.isNotEmpty ? '未找到相关研报' : '当前筛选下暂无研报', + message: currentQuery.isNotEmpty ? '换个关键词试试' : '调整筛选条件后再试', + actionLabel: '清除筛选', + onAction: () { + searchController.clear(); + query.value = ''; + topic.value = ''; + hasAudio.value = false; + }, + ) + else + for (final report in filtered) ...[ + ReportCardWidget( + report: report, + onTap: () => openReportDetail( + context, + dataSource, + report, + player: player, + onStartAudio: onStartModuleAudio, + onToggleAudio: onToggleAudio, + onSeekAudio: onSeekAudio, + onSpeed: onSpeed, + ), + onPlayTap: () => onPlay( + AudioItem( + audioId: 'local_${report.id}', + reportId: report.id, + titleCn: report.titleCn, + reportTitleCn: report.titleCn, + durationSec: 180, + institution: report.institution, + ), ), ), - ), - const SizedBox(height: WiseSpacing.x3), - ], - ], + const SizedBox(height: YantingSpacing.x3), + ], + ], + ), ); }, ); @@ -194,45 +216,65 @@ void _openFilterSheet( required ValueNotifier topic, }) { final topics = {for (final item in items) ...item.topics}.toList(); - showModalBottomSheet( + showShadSheet( context: context, - showDragHandle: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)), - ), - builder: (context) => Padding( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 28), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('筛选研报', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: WiseSpacing.x3), - Wrap( - spacing: WiseSpacing.x2, - runSpacing: WiseSpacing.x2, - children: [ - AppChip( - label: '全部主题', - selected: topic.value.isEmpty, - onTap: () => topic.value = '', - ), - for (final t in topics) - AppChip( - label: t, - selected: topic.value == t, - onTap: () => topic.value = t, + side: ShadSheetSide.bottom, + builder: (context) { + final theme = ShadTheme.of(context); + final selectedBackground = theme.colorScheme.foreground; + final selectedForeground = theme.colorScheme.background; + final unselectedBackground = theme.colorScheme.secondary; + final unselectedForeground = theme.colorScheme.secondaryForeground; + + return ShadSheet( + title: const Text('筛选研报'), + description: const Text('按主题快速收窄列表。'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: YantingSpacing.x2, + runSpacing: YantingSpacing.x2, + children: [ + ShadBadge.secondary( + onPressed: () => topic.value = '', + backgroundColor: topic.value.isEmpty + ? selectedBackground + : unselectedBackground, + foregroundColor: topic.value.isEmpty + ? selectedForeground + : unselectedForeground, + hoverBackgroundColor: topic.value.isEmpty + ? selectedBackground.withValues(alpha: 0.9) + : theme.colorScheme.border, + child: const Text('全部主题'), ), - ], - ), - const SizedBox(height: WiseSpacing.x4), - AppButton( - label: '完成', - expand: true, - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), + for (final t in topics) + ShadBadge.secondary( + onPressed: () => topic.value = t, + backgroundColor: topic.value == t + ? selectedBackground + : unselectedBackground, + foregroundColor: topic.value == t + ? selectedForeground + : unselectedForeground, + hoverBackgroundColor: topic.value == t + ? selectedBackground.withValues(alpha: 0.9) + : theme.colorScheme.border, + child: Text(t), + ), + ], + ), + const SizedBox(height: 12), + ShadButton( + width: double.infinity, + onPressed: () => Navigator.pop(context), + child: const Text('完成'), + ), + ], + ), + ); + }, ); } diff --git a/lib/features/shell_page.dart b/lib/features/shell_page.dart index 030fe9c..f806ad1 100644 --- a/lib/features/shell_page.dart +++ b/lib/features/shell_page.dart @@ -1,10 +1,11 @@ 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 '../data/providers.dart'; -import '../theme/wise_tokens.dart'; +import '../routing/app_routes.dart'; +import '../theme/yanting_text.dart'; import '../widgets/bottom_tab_bar.dart'; import '../widgets/mini_player.dart'; @@ -16,30 +17,42 @@ class ShellPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final theme = ShadTheme.of(context); final player = ref.watch(audioPlayerControllerProvider); final controller = ref.read(audioPlayerControllerProvider.notifier); + final canPop = GoRouter.of(context).canPop(); final selectedIndex = _tabs.indexWhere((tab) => tab.path == currentPath); final safeIndex = selectedIndex < 0 ? 0 : selectedIndex; return Scaffold( + backgroundColor: theme.colorScheme.background, appBar: AppBar( - title: const Column( + backgroundColor: theme.colorScheme.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + leading: canPop + ? ShadIconButton.ghost( + onPressed: () => context.pop(), + icon: const Icon(LucideIcons.chevronLeft, size: 18), + ) + : null, + title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('研听'), - Text( - '全球机构研报中文解读', - style: TextStyle( - fontSize: 12, - color: WiseColors.textSecondary, - fontWeight: FontWeight.w500, - ), - ), + Text('研听', style: YantingText.listTitle), + Text('全球机构研报中文解读', style: YantingText.meta.copyWith(fontSize: 12)), ], ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: ColoredBox( + color: theme.colorScheme.border, + child: const SizedBox(height: 1, width: double.infinity), + ), + ), ), body: ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, + color: theme.colorScheme.background, child: Stack(children: [Positioned.fill(child: child)]), ), bottomNavigationBar: SafeArea( diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart index 8510090..c148709 100644 --- a/lib/routing/app_router.dart +++ b/lib/routing/app_router.dart @@ -1,17 +1,18 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; import '../data/providers.dart'; import '../features/detail/report_detail_page.dart'; import '../features/feed/feed_page.dart'; +import '../features/home/home_page.dart'; import '../features/institutions/institution_detail_page.dart'; 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/shell_page.dart'; -import '../theme/wise_tokens.dart'; import 'app_routes.dart'; final routerProvider = Provider((ref) { @@ -46,6 +47,28 @@ final routerProvider = Provider((ref) { ), ), ), + GoRoute( + path: AppRoutes.homeFeed, + builder: (context, state) => _TabSurface( + child: Consumer( + builder: (context, ref, _) { + final player = ref.watch(audioPlayerControllerProvider); + final controller = ref.read( + audioPlayerControllerProvider.notifier, + ); + return HomePage( + dataSource: dataSource, + onPlay: controller.startFromItem, + player: player, + onStartModuleAudio: controller.startModuleAudio, + onToggleAudio: controller.toggleAudio, + onSeekAudio: controller.seekAudio, + onSpeed: controller.cycleSpeed, + ); + }, + ), + ), + ), GoRoute( path: AppRoutes.reports, builder: (context, state) => _TabSurface( @@ -142,6 +165,9 @@ class _TabSurface extends StatelessWidget { @override Widget build(BuildContext context) { - return ColoredBox(color: WiseColors.canvas, child: child); + return ColoredBox( + color: ShadTheme.of(context).colorScheme.background, + child: child, + ); } } diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index b6a3ddb..939a72d 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -7,6 +7,7 @@ import '../widgets/mini_player.dart'; abstract final class AppRoutes { static const home = '/'; + static const homeFeed = '/feed'; static const reports = '/reports'; static const institutions = '/institutions'; static const listen = '/listen'; @@ -53,12 +54,7 @@ void openReportDetail( ReportDataSource dataSource, ReportCardModel report, { PlayerStateModel player = const PlayerStateModel(), - void Function( - String audioId, - String reportId, - String title, - int durationSec, - )? + void Function(String audioId, String reportId, String title, int durationSec)? onStartAudio, VoidCallback? onToggleAudio, void Function(int delta)? onSeekAudio, diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index a4844fb..d896dac 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'yanting_text.dart'; import 'yanting_tokens.dart'; -import 'wise_tokens.dart'; ThemeData buildAppTheme() { final scheme = ColorScheme.fromSeed( @@ -29,13 +28,14 @@ ThemeData buildAppTheme() { elevation: 0, centerTitle: false, titleTextStyle: YantingText.sectionTitle, + surfaceTintColor: Colors.transparent, ), cardTheme: const CardThemeData( color: YantingColors.card, elevation: 0, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(WiseRadius.md)), + borderRadius: BorderRadius.all(Radius.circular(YantingRadius.xl)), side: BorderSide(color: YantingColors.border), ), ), @@ -64,6 +64,16 @@ ThemeData buildAppTheme() { borderSide: const BorderSide(color: YantingColors.foreground), ), ), + snackBarTheme: SnackBarThemeData( + backgroundColor: YantingColors.foreground, + contentTextStyle: YantingText.body.copyWith( + color: YantingColors.background, + ), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(YantingRadius.md), + ), + ), navigationBarTheme: NavigationBarThemeData( backgroundColor: YantingColors.background, indicatorColor: Colors.transparent, diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index fc76896..8ae46e8 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,2 +1,5 @@ export 'app_theme.dart'; +export 'yanting_shad_theme.dart'; +export 'yanting_text.dart'; +export 'yanting_tokens.dart'; export 'wise_tokens.dart'; diff --git a/lib/theme/yanting_shad_theme.dart b/lib/theme/yanting_shad_theme.dart new file mode 100644 index 0000000..87991e8 --- /dev/null +++ b/lib/theme/yanting_shad_theme.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +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, + }, +); + +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 buildYantingShadTheme() { + return ShadThemeData( + brightness: Brightness.light, + colorScheme: _lightShadColors, + radius: BorderRadius.circular(YantingRadius.base), + cardTheme: ShadCardTheme( + padding: const EdgeInsets.all(YantingSpacing.cardPadding), + radius: BorderRadius.circular(YantingRadius.xl), + border: ShadBorder.all(color: YantingColors.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, + ), + ); +} diff --git a/lib/theme/yanting_tokens.dart b/lib/theme/yanting_tokens.dart index b8ec279..4c66350 100644 --- a/lib/theme/yanting_tokens.dart +++ b/lib/theme/yanting_tokens.dart @@ -2,20 +2,20 @@ import 'package:flutter/material.dart'; abstract final class YantingColors { static const background = Color(0xFFFFFFFF); - static const foreground = Color(0xFF171717); + static const foreground = Color(0xFF1A1A1A); static const card = Color(0xFFFFFFFF); - static const primary = Color(0xFFA3E635); - static const primaryForeground = Color(0xFF3F6212); + static const primary = Color(0xFF95E300); + static const primaryForeground = Color(0xFF365314); static const secondary = Color(0xFFF4F4F5); static const secondaryForeground = Color(0xFF27272A); - static const muted = Color(0xFFF5F5F5); - static const mutedForeground = Color(0xFF737373); + static const muted = Color(0xFFF7F7F7); + static const mutedForeground = Color(0xFF71717A); static const border = Color(0xFFE5E5E5); static const input = Color(0xFFE5E5E5); static const destructive = Color(0xFFEF4444); - static const warning = Color(0xFF9A6A00); + static const warning = Color(0xFF9A6500); static const chart2 = Color(0xFF84CC16); - static const brandSoft = Color(0xFFEEFBD8); + static const brandSoft = Color(0xFFECFCCB); static const brandSoftBorder = Color(0xFFD6F5A8); static const link = Color(0xFF2563EB); static const canvas = background; diff --git a/lib/widgets/app_buttons.dart b/lib/widgets/app_buttons.dart index 34b5b13..70d4d36 100644 --- a/lib/widgets/app_buttons.dart +++ b/lib/widgets/app_buttons.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; -import '../theme/yanting_text.dart'; import '../theme/yanting_tokens.dart'; class AppButton extends StatelessWidget { @@ -21,48 +21,84 @@ class AppButton extends StatelessWidget { @override Widget build(BuildContext context) { - final colors = switch (kind) { - AppButtonKind.primary => ( - YantingColors.primary, - YantingColors.primaryForeground, - Colors.transparent, + final leading = icon == null ? null : Icon(icon, size: 16); + final width = expand ? double.infinity : null; + final child = Text(label); + + return switch (kind) { + AppButtonKind.primary => ShadButton( + width: width, + onPressed: onPressed, + leading: leading, + child: child, ), - AppButtonKind.dark => ( - YantingColors.foreground, - YantingColors.background, - Colors.transparent, + AppButtonKind.dark => ShadButton( + width: width, + onPressed: onPressed, + leading: leading, + backgroundColor: YantingColors.foreground, + foregroundColor: YantingColors.background, + hoverBackgroundColor: YantingColors.foreground.withValues(alpha: 0.9), + child: child, ), - AppButtonKind.accent => ( - YantingColors.brandSoft, - YantingColors.primaryForeground, - Colors.transparent, + AppButtonKind.accent => ShadButton.secondary( + width: width, + onPressed: onPressed, + leading: leading, + backgroundColor: YantingColors.brandSoft, + foregroundColor: YantingColors.primaryForeground, + hoverBackgroundColor: YantingColors.brandSoftBorder, + child: child, ), - AppButtonKind.ghost => ( - YantingColors.background, - YantingColors.foreground, - YantingColors.border, + AppButtonKind.ghost => ShadButton.outline( + width: width, + onPressed: onPressed, + leading: leading, + child: child, ), }; - final child = FilledButton.icon( - onPressed: onPressed, - icon: icon == null ? const SizedBox.shrink() : Icon(icon, size: 18), - label: Text(label), - style: FilledButton.styleFrom( - backgroundColor: colors.$1, - foregroundColor: colors.$2, - disabledBackgroundColor: YantingColors.border, - disabledForegroundColor: YantingColors.mutedForeground, - minimumSize: Size(expand ? double.infinity : 0, 44), - textStyle: YantingText.body.copyWith(fontWeight: FontWeight.w600), - side: colors.$3 == Colors.transparent - ? BorderSide.none - : BorderSide(color: colors.$3), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(YantingRadius.md), - ), + } +} + +class AppIconButton extends StatelessWidget { + const AppIconButton({ + required this.icon, + required this.onPressed, + this.kind = AppButtonKind.ghost, + super.key, + }); + + final IconData icon; + final VoidCallback? onPressed; + final AppButtonKind kind; + + @override + Widget build(BuildContext context) { + final iconWidget = Icon(icon, size: 16); + return switch (kind) { + AppButtonKind.primary => ShadIconButton( + onPressed: onPressed, + icon: iconWidget, ), - ); - return expand ? SizedBox(width: double.infinity, child: child) : child; + AppButtonKind.dark => ShadIconButton( + onPressed: onPressed, + backgroundColor: YantingColors.foreground, + foregroundColor: YantingColors.background, + hoverBackgroundColor: YantingColors.foreground.withValues(alpha: 0.9), + icon: iconWidget, + ), + AppButtonKind.accent => ShadIconButton.secondary( + onPressed: onPressed, + backgroundColor: YantingColors.brandSoft, + foregroundColor: YantingColors.primaryForeground, + hoverBackgroundColor: YantingColors.brandSoftBorder, + icon: iconWidget, + ), + AppButtonKind.ghost => ShadIconButton.outline( + onPressed: onPressed, + icon: iconWidget, + ), + }; } } diff --git a/lib/widgets/app_card.dart b/lib/widgets/app_card.dart index 3e6bbd5..bd7ca0d 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,29 +21,29 @@ class AppCard extends StatelessWidget { @override Widget build(BuildContext context) { - final decoration = BoxDecoration( - color: color, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(YantingRadius.xl), - ); - final content = DecoratedBox( - decoration: BoxDecoration( - color: color, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(YantingRadius.xl), - ), - child: Padding(padding: padding, child: child), + final theme = ShadTheme.of(context); + final radius = BorderRadius.circular(YantingRadius.xl); + final content = ShadCard( + padding: padding, + backgroundColor: color, + radius: radius, + border: ShadBorder.all(color: borderColor), + shadows: const [], + child: child, ); if (onTap == null) return content; + return Material( color: Colors.transparent, - child: Ink( - decoration: decoration, - child: InkWell( - borderRadius: BorderRadius.circular(YantingRadius.xl), - onTap: onTap, - child: Padding(padding: padding, child: child), + borderRadius: radius, + child: InkWell( + borderRadius: radius, + splashColor: theme.colorScheme.mutedForeground.withValues(alpha: 0.08), + highlightColor: theme.colorScheme.mutedForeground.withValues( + alpha: 0.04, ), + onTap: onTap, + child: content, ), ); } diff --git a/lib/widgets/badges.dart b/lib/widgets/badges.dart index e10abd1..8f18e44 100644 --- a/lib/widgets/badges.dart +++ b/lib/widgets/badges.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; -import '../theme/yanting_text.dart'; import '../theme/yanting_tokens.dart'; class AppBadge extends StatelessWidget { @@ -17,60 +17,45 @@ class AppBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final colors = switch (kind) { - BadgeKind.brand => ( - YantingColors.primary, - YantingColors.primaryForeground, - Colors.transparent, + final child = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[Icon(icon, size: 12), const SizedBox(width: 4)], + Text(text), + ], + ); + final shape = RoundedRectangleBorder( + borderRadius: BorderRadius.circular(YantingRadius.sm), + side: kind == BadgeKind.tier || kind == BadgeKind.warning + ? const BorderSide(color: YantingColors.border) + : BorderSide.none, + ); + + return switch (kind) { + BadgeKind.brand => ShadBadge( + shape: shape, + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + child: child, ), - BadgeKind.audio => ( - YantingColors.secondary, - YantingColors.secondaryForeground, - Colors.transparent, + BadgeKind.audio || BadgeKind.neutral => ShadBadge.secondary( + shape: shape, + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + child: child, ), - BadgeKind.tier => ( - YantingColors.background, - YantingColors.mutedForeground, - YantingColors.border, + BadgeKind.tier => ShadBadge.outline( + shape: shape, + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + foregroundColor: YantingColors.mutedForeground, + child: child, ), - BadgeKind.warning => ( - YantingColors.background, - YantingColors.destructive, - YantingColors.border, - ), - BadgeKind.neutral => ( - YantingColors.secondary, - YantingColors.secondaryForeground, - Colors.transparent, + BadgeKind.warning => ShadBadge.destructive( + shape: shape, + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + backgroundColor: YantingColors.background, + foregroundColor: YantingColors.destructive, + child: child, ), }; - return DecoratedBox( - decoration: BoxDecoration( - color: colors.$1, - border: colors.$3 == Colors.transparent - ? null - : Border.all(color: colors.$3), - borderRadius: BorderRadius.circular(YantingRadius.sm), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) ...[ - Icon(icon, size: 14, color: colors.$2), - const SizedBox(width: 4), - ], - Text( - text, - style: - (Theme.of(context).textTheme.labelSmall ?? YantingText.badge) - .copyWith(color: colors.$2, letterSpacing: 0), - ), - ], - ), - ), - ); } } @@ -90,33 +75,20 @@ class AppChip extends StatelessWidget { @override Widget build(BuildContext context) { - final background = selected - ? YantingColors.foreground - : YantingColors.secondary; - final foreground = selected - ? YantingColors.background - : YantingColors.secondaryForeground; - return Material( - color: Colors.transparent, - child: Ink( - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(YantingRadius.pill), - ), - child: InkWell( - borderRadius: BorderRadius.circular(YantingRadius.pill), - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 9), - child: Text( - label, - style: - (Theme.of(context).textTheme.labelLarge ?? YantingText.chip) - .copyWith(color: foreground, letterSpacing: 0), - ), - ), - ), - ), + return ShadBadge.secondary( + onPressed: onTap, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 9), + backgroundColor: selected + ? YantingColors.foreground + : YantingColors.secondary, + hoverBackgroundColor: selected + ? YantingColors.foreground.withValues(alpha: 0.9) + : YantingColors.border, + foregroundColor: selected + ? YantingColors.background + : YantingColors.secondaryForeground, + child: Text(label), ); } } diff --git a/lib/widgets/mini_player.dart b/lib/widgets/mini_player.dart index 31cfc15..d92e900 100644 --- a/lib/widgets/mini_player.dart +++ b/lib/widgets/mini_player.dart @@ -1,10 +1,12 @@ 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 'app_buttons.dart'; import 'app_card.dart'; class PlayerStateModel { @@ -126,14 +128,12 @@ class MiniPlayer extends StatelessWidget { ], ), ), - IconButton( + ShadIconButton.ghost( onPressed: onToggle, icon: Icon( player.playing ? AppIcons.pause : AppIcons.playCircle, size: player.playing ? 24 : 28, ), - color: YantingColors.foreground, - visualDensity: VisualDensity.compact, ), ], ), @@ -184,13 +184,7 @@ class PlayerCard extends StatelessWidget { style: YantingText.meta.copyWith(fontSize: 12.5), ), const SizedBox(height: 16), - LinearProgressIndicator( - value: ratio.clamp(0, 1), - minHeight: 4, - borderRadius: BorderRadius.circular(YantingRadius.pill), - backgroundColor: YantingColors.border, - color: YantingColors.primary, - ), + ShadProgress(value: ratio.clamp(0, 1)), const SizedBox(height: WiseSpacing.x2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -216,18 +210,15 @@ class PlayerCard extends StatelessWidget { children: [ _SkipButton(label: '-15', onPressed: () => onSeek(-15)), const SizedBox(width: 26), - IconButton.filled( - onPressed: active ? onToggle : onStart, - icon: Icon( - active && player.playing + SizedBox( + width: 56, + height: 56, + child: AppIconButton( + kind: AppButtonKind.primary, + onPressed: active ? onToggle : onStart, + icon: active && player.playing ? AppIcons.pause : AppIcons.play, - size: 28, - ), - style: IconButton.styleFrom( - backgroundColor: YantingColors.primary, - foregroundColor: YantingColors.primaryForeground, - fixedSize: const Size(56, 56), ), ), const SizedBox(width: 26), @@ -236,22 +227,10 @@ class PlayerCard extends StatelessWidget { ), Align( alignment: Alignment.centerRight, - child: TextButton( + child: ShadButton.outline( + size: ShadButtonSize.sm, onPressed: onSpeed, - style: TextButton.styleFrom( - backgroundColor: YantingColors.background, - foregroundColor: YantingColors.foreground, - side: const BorderSide(color: YantingColors.border), - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(horizontal: 12), - ), - child: Text( - '${player.speed.toStringAsFixed(1)}x', - style: YantingText.meta.copyWith( - color: YantingColors.foreground, - fontWeight: FontWeight.w600, - ), - ), + child: Text('${player.speed.toStringAsFixed(1)}x'), ), ), ], diff --git a/lib/widgets/sheets.dart b/lib/widgets/sheets.dart index f698c18..85cd844 100644 --- a/lib/widgets/sheets.dart +++ b/lib/widgets/sheets.dart @@ -1,27 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; -import '../theme/wise_tokens.dart'; import 'app_buttons.dart'; import 'states.dart'; -Future showLoginSheet(BuildContext context, {String reason = '登录后保存当前动作'}) { - return showModalBottomSheet( +Future showLoginSheet( + BuildContext context, { + String reason = '登录后保存当前动作', +}) { + return showShadSheet( context: context, - showDragHandle: true, - backgroundColor: WiseColors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)), - ), - builder: (context) => Padding( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 28), + side: ShadSheetSide.bottom, + builder: (context) => ShadSheet( + title: const Text('登录研听'), + description: Text(reason), child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('登录研听', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: WiseSpacing.x2), - Text(reason, style: Theme.of(context).textTheme.bodyMedium), - const SizedBox(height: WiseSpacing.x4), AppButton( label: '使用手机号继续', icon: Icons.phone_iphone, @@ -31,7 +26,7 @@ Future showLoginSheet(BuildContext context, {String reason = '登录后保 showAppToast(context, '登录接口待接入,已保留当前页面'); }, ), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: 8), AppButton( label: '微信 / Apple 登录占位', icon: Icons.account_circle_outlined, @@ -49,37 +44,21 @@ Future showLoginSheet(BuildContext context, {String reason = '登录后保 } Future showOutboundSheet(BuildContext context, {required String title}) { - return showModalBottomSheet( + return showShadSheet( context: context, - showDragHandle: true, - backgroundColor: WiseColors.surface, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(WiseRadius.lg)), - ), - builder: (context) => Padding( - padding: const EdgeInsets.fromLTRB(20, 4, 20, 28), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('即将打开外部服务', style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: WiseSpacing.x2), - Text( - '$title\n外跳仅用于了解原文或相关服务,本内容不构成投资建议。', - style: Theme.of(context).textTheme.bodyMedium, - ), - const SizedBox(height: WiseSpacing.x4), - AppButton( - label: '确认并记录占位事件', - icon: Icons.open_in_new, - kind: AppButtonKind.accent, - expand: true, - onPressed: () { - Navigator.pop(context); - showAppToast(context, '外跳事件接口待接入'); - }, - ), - ], + side: ShadSheetSide.bottom, + builder: (context) => ShadSheet( + title: const Text('即将打开外部服务'), + description: Text('$title\n外跳仅用于了解原文或相关服务,本内容不构成投资建议。'), + child: AppButton( + label: '确认并记录占位事件', + icon: Icons.open_in_new, + kind: AppButtonKind.accent, + expand: true, + onPressed: () { + Navigator.pop(context); + showAppToast(context, '外跳事件接口待接入'); + }, ), ), ); diff --git a/lib/widgets/states.dart b/lib/widgets/states.dart index bb5efdc..3a08e97 100644 --- a/lib/widgets/states.dart +++ b/lib/widgets/states.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; -import '../theme/wise_tokens.dart'; +import '../theme/yanting_tokens.dart'; import 'app_buttons.dart'; import 'app_card.dart'; @@ -12,9 +13,10 @@ class LoadingState extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( - padding: const EdgeInsets.all(WiseSpacing.x4), + padding: const EdgeInsets.all(YantingSpacing.screenX), itemCount: 4, - separatorBuilder: (_, _) => const SizedBox(height: WiseSpacing.x3), + separatorBuilder: (_, _) => + const SizedBox(height: YantingSpacing.cardGap), itemBuilder: (context, index) => const SkeletonCard(), ); } @@ -30,11 +32,11 @@ class SkeletonCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: const [ SkeletonLine(width: 96), - SizedBox(height: WiseSpacing.x3), + SizedBox(height: YantingSpacing.cardGap), SkeletonLine(width: double.infinity, height: 18), - SizedBox(height: WiseSpacing.x2), + SizedBox(height: YantingSpacing.x2), SkeletonLine(width: 240), - SizedBox(height: WiseSpacing.x3), + SizedBox(height: YantingSpacing.cardGap), SkeletonLine(width: 160), ], ), @@ -50,12 +52,58 @@ class SkeletonLine extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + final theme = ShadTheme.of(context); + return _PulsingSkeleton( width: width, height: height, - decoration: BoxDecoration( - color: WiseColors.border, - borderRadius: BorderRadius.circular(WiseRadius.pill), + color: theme.colorScheme.muted, + ); + } +} + +class _PulsingSkeleton extends StatefulWidget { + const _PulsingSkeleton({ + required this.color, + required this.width, + required this.height, + }); + + final Color color; + final double width; + final double height; + + @override + State<_PulsingSkeleton> createState() => _PulsingSkeletonState(); +} + +class _PulsingSkeletonState extends State<_PulsingSkeleton> + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = ShadTheme.of(context); + return FadeTransition( + opacity: Tween( + begin: 0.4, + end: 1, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)), + child: Container( + width: widget.width, + height: widget.height, + decoration: BoxDecoration( + color: widget.color, + borderRadius: theme.radius, + ), ), ); } @@ -79,24 +127,29 @@ class EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = ShadTheme.of(context); return Center( child: Padding( - padding: const EdgeInsets.all(WiseSpacing.x6), + padding: const EdgeInsets.all(YantingSpacing.x6), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 42, color: WiseColors.primary), - const SizedBox(height: WiseSpacing.x3), + Icon(icon, size: 42, color: theme.colorScheme.foreground), + const SizedBox(height: YantingSpacing.cardGap), Text(title, style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: WiseSpacing.x2), + const SizedBox(height: YantingSpacing.x2), Text( message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), if (actionLabel != null) ...[ - const SizedBox(height: WiseSpacing.x4), - AppButton(label: actionLabel!, onPressed: onAction, kind: AppButtonKind.ghost), + const SizedBox(height: YantingSpacing.x4), + AppButton( + label: actionLabel!, + onPressed: onAction, + kind: AppButtonKind.ghost, + ), ], ], ), @@ -124,12 +177,5 @@ class ErrorState extends StatelessWidget { } void showAppToast(BuildContext context, String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - behavior: SnackBarBehavior.floating, - backgroundColor: WiseColors.primary, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(WiseRadius.md)), - ), - ); + ShadToaster.of(context).show(ShadToast(title: Text(message))); } diff --git a/pubspec.lock b/pubspec.lock index 4584279..2112885 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -17,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + boxy: + dependency: transitive + description: + name: boxy + sha256: "42ccafe13b2893878042acc5b7e2446025328e11a3197b0bb78db42ff76aa3f0" + url: "https://pub.dev" + source: hosted + version: "2.3.0" characters: dependency: transitive description: @@ -41,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -49,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + extended_image: + dependency: transitive + description: + name: extended_image + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 + url: "https://pub.dev" + source: hosted + version: "10.0.1" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" + url: "https://pub.dev" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: @@ -57,11 +97,27 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_hooks: dependency: "direct main" description: @@ -91,6 +147,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -109,6 +181,14 @@ packages: url: "https://pub.dev" source: hosted version: "16.3.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" hooks_riverpod: dependency: "direct main" description: @@ -125,6 +205,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://pub.dev" + source: hosted + version: "3.0.0" http_parser: dependency: transitive description: @@ -141,6 +229,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" leak_tracker: dependency: transitive description: @@ -181,6 +293,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + lucide_icons_flutter: + dependency: transitive + description: + name: lucide_icons_flutter + sha256: "7c5dc01a32a9905ae34e2d84224e92d6d0c42acf8926df9e01c35a1446bf1b69" + url: "https://pub.dev" + source: hosted + version: "3.1.14+2" matcher: dependency: transitive description: @@ -205,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -213,6 +341,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" phosphor_flutter: dependency: "direct main" description: @@ -221,6 +413,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" remixicon: dependency: "direct main" description: @@ -237,11 +445,43 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + serial_csv: + dependency: transitive + description: + name: serial_csv + sha256: "2d62bb70cb3ce7251383fc86ea9aae1298ab1e57af6ef4e93b6a9751c5c268dd" + url: "https://pub.dev" + source: hosted + version: "0.5.2" + shadcn_ui: + dependency: "direct main" + description: + name: shadcn_ui + sha256: "6c06f2bcebd8734b9ed0bf3f63ef5c71981573d5664923589b0302f8280e7eaf" + url: "https://pub.dev" + source: hosted + version: "0.53.6" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + slang: + dependency: transitive + description: + name: slang + sha256: "46e929158c2f563994c4d1fce5819cfa13e18b164941473d2553bcddcf387c31" + url: "https://pub.dev" + source: hosted + version: "4.15.0" + slang_flutter: + dependency: transitive + description: + name: slang_flutter + sha256: "0eb6348416a296f1bd940fe02669bcd2df5c5cfdabf893b98e448df8b7ecf4ac" + url: "https://pub.dev" + source: hosted + version: "4.15.0" source_span: dependency: transitive description: @@ -298,6 +538,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + theme_extensions_builder_annotation: + dependency: transitive + description: + name: theme_extensions_builder_annotation + sha256: "75f28ac85d396d143d111a47c1395b01f3be41b7135f37bd51512921944e4206" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + two_dimensional_scrollables: + dependency: transitive + description: + name: two_dimensional_scrollables + sha256: "4f25bd42783626c5f2810333418727455397195acb61e53710e638a6a98e0e5e" + url: "https://pub.dev" + source: hosted + version: "0.5.2" typed_data: dependency: transitive description: @@ -306,6 +562,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_image: + dependency: transitive + description: + name: universal_image + sha256: "2eae13df84a47960cc4148fec88f09031ea64cbf667b4131ff2c907dd7b7c6d1" + url: "https://pub.dev" + source: hosted + version: "1.0.12" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.dev" + source: hosted + version: "1.2.3" vector_math: dependency: transitive description: @@ -322,6 +610,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: @@ -330,6 +626,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.32.0" + flutter: ">=3.35.6" diff --git a/pubspec.yaml b/pubspec.yaml index d56ef5c..582e58d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,8 +39,10 @@ dependencies: flutter_hooks: ^0.21.3+1 go_router: ^16.2.4 hooks_riverpod: ^2.6.1 + google_fonts: ^6.2.1 phosphor_flutter: ^2.1.0 remixicon: ^4.9.3 + shadcn_ui: ^0.53.6 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index 318bcc2..336fe1f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -24,7 +24,7 @@ void main() { await tester.tap(find.text('报告摘要').last); await tester.pumpAndSettle(); - expect(find.text('报告摘要'), findsOneWidget); + expect(find.text('报告摘要'), findsWidgets); expect(find.text('需求结构'), findsOneWidget); }); }