鸿蒙提交

This commit is contained in:
Developer
2026-06-12 22:30:26 +08:00
parent f91be94e9c
commit 82bb3e2f14
5 changed files with 589 additions and 269 deletions

View File

@@ -109,13 +109,13 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
parent: _stepAnimController,
curve: Curves.easeOut,
);
_stepSlideAnimation = Tween<Offset>(
begin: const Offset(0.15, 0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _stepAnimController,
curve: Curves.easeOutCubic,
));
_stepSlideAnimation =
Tween<Offset>(begin: const Offset(0.15, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _stepAnimController,
curve: Curves.easeOutCubic,
),
);
_stepAnimController.value = 1.0;
}
@@ -239,11 +239,8 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
),
),
GestureDetector(
onTap: () => RegisterDialogs.showRegisterTips(
context,
ext: ext,
auth: auth,
),
onTap: () =>
RegisterDialogs.showRegisterTips(context, ext: ext, auth: auth),
child: Icon(
CupertinoIcons.info_circle,
size: 22,
@@ -301,6 +298,10 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
_goToStep(2);
_sendCode(_emailController.text.trim());
},
agreedToTerms: _agreedToTerms,
onToggleAgreement: () =>
setState(() => _agreedToTerms = !_agreedToTerms),
onShowAgreement: _showAgreement,
ext: ext,
auth: auth,
);
@@ -372,15 +373,12 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
subscribeEmail: _subscribeEmail,
onToggleSubscribe: () =>
setState(() => _subscribeEmail = !_subscribeEmail),
agreedToTerms: _agreedToTerms,
onToggleAgreement: () =>
setState(() => _agreedToTerms = !_agreedToTerms),
onShowAgreement: _showAgreement,
onPrev: () => _goToStep(2),
onBack: () => _goToStep(1),
onRegister: _handleRegister,
isLoading: authState.isLoading,
canRegister: _agreedToTerms &&
canRegister:
_agreedToTerms &&
_regPasswordController.text.isNotEmpty &&
_regConfirmPasswordController.text.isNotEmpty &&
!authState.isLoading,
@@ -495,7 +493,12 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
}
/// 处理注册错误
void _handleRegisterError(Object e, String email, String username, String password) {
void _handleRegisterError(
Object e,
String email,
String username,
String password,
) {
final errorMsg = e.toString();
if (errorMsg.contains('已注册') ||
errorMsg.contains('already') ||
@@ -619,10 +622,9 @@ class _RegisterSectionState extends ConsumerState<RegisterSection>
) async {
final t = ref.read(translationsProvider);
try {
final success = await ref.read(authProvider.notifier).login(
account: username,
password: password,
);
final success = await ref
.read(authProvider.notifier)
.login(account: username, password: password);
if (success && mounted) {
AppToast.showSuccess(t.auth.registerSuccess);
widget.onRegisterSuccess();

View File

@@ -1,9 +1,9 @@
/// ============================================================
/// 闲言APP — 注册步骤1账号邮箱输入
/// 创建时间: 2026-06-08
/// 更新时间: 2026-06-08
/// 作用: 注册流程第一步,输入用户名和邮箱,含格式校验可用性检测
/// 上次更新: 增加用户名格式校验、实时可用性防抖检测
/// 更新时间: 2026-06-12
/// 作用: 注册流程第一步,输入用户名和邮箱,含格式校验可用性检测及协议勾选
/// 上次更新: 增加用户协议勾选框,勾选后才允许进入下一步
/// ============================================================
import 'dart:async';
@@ -40,13 +40,16 @@ enum UsernameStatus {
taken,
}
/// 注册步骤1 — 账号邮箱输入
/// 注册步骤1 — 账号邮箱输入(含协议勾选)
class RegisterStepAccount extends StatefulWidget {
const RegisterStepAccount({
super.key,
required this.usernameController,
required this.emailController,
required this.onNext,
required this.agreedToTerms,
required this.onToggleAgreement,
required this.onShowAgreement,
required this.ext,
required this.auth,
});
@@ -54,6 +57,9 @@ class RegisterStepAccount extends StatefulWidget {
final TextEditingController usernameController;
final TextEditingController emailController;
final VoidCallback onNext;
final bool agreedToTerms;
final VoidCallback onToggleAgreement;
final void Function(bool isUserAgreement) onShowAgreement;
final AppThemeExtension ext;
final TAuth auth;
@@ -143,7 +149,9 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
final taken = await UserSecurityService.checkUsername(username);
if (!mounted) return;
setState(() {
_usernameStatus = taken ? UsernameStatus.taken : UsernameStatus.available;
_usernameStatus = taken
? UsernameStatus.taken
: UsernameStatus.available;
_usernameError = taken ? auth.usernameTaken : '';
});
} catch (e) {
@@ -157,14 +165,15 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
}
}
/// 是否可以进入下一步
/// 是否可以进入下一步(需填写完整 + 用户名合法 + 已勾选协议)
bool get _canProceed {
final username = widget.usernameController.text.trim();
final email = widget.emailController.text.trim();
return username.isNotEmpty &&
email.isNotEmpty &&
_usernameStatus != UsernameStatus.invalid &&
_usernameStatus != UsernameStatus.taken;
_usernameStatus != UsernameStatus.taken &&
widget.agreedToTerms;
}
/// 点击下一步
@@ -172,6 +181,12 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
final username = widget.usernameController.text.trim();
final email = widget.emailController.text.trim();
// 协议勾选校验
if (!widget.agreedToTerms) {
AppToast.showWarning(auth.pleaseAgreeTerms);
return;
}
// 二次格式校验
if (username.isEmpty || email.isEmpty) {
AppToast.showWarning(auth.pleaseFillRequired);
@@ -220,6 +235,9 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: AppSpacing.lg),
// 协议同意行
_buildAgreementRow(),
const SizedBox(height: AppSpacing.lg),
// 下一步按钮
SizedBox(
width: double.infinity,
@@ -272,9 +290,7 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
const SizedBox(width: AppSpacing.sm),
Text(
auth.usernameChecking,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
),
],
),
@@ -288,14 +304,16 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
padding: const EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
Icon(CupertinoIcons.xmark_circle_fill, size: 14, color: ext.errorColor),
Icon(
CupertinoIcons.xmark_circle_fill,
size: 14,
color: ext.errorColor,
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
_usernameError,
style: AppTypography.caption1.copyWith(
color: ext.errorColor,
),
style: AppTypography.caption1.copyWith(color: ext.errorColor),
),
),
],
@@ -309,13 +327,15 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
padding: const EdgeInsets.only(top: AppSpacing.xs),
child: Row(
children: [
Icon(CupertinoIcons.checkmark_circle_fill, size: 14, color: ext.successColor),
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 14,
color: ext.successColor,
),
const SizedBox(width: AppSpacing.sm),
Text(
auth.username,
style: AppTypography.caption1.copyWith(
color: ext.successColor,
),
style: AppTypography.caption1.copyWith(color: ext.successColor),
),
],
),
@@ -324,4 +344,89 @@ class _RegisterStepAccountState extends State<RegisterStepAccount> {
return const SizedBox.shrink();
}
/// 协议同意行(用户协议 + 隐私政策)
Widget _buildAgreementRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.onToggleAgreement,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
child: Center(
child: Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(top: 1),
decoration: BoxDecoration(
color: widget.agreedToTerms
? ext.accent
: CupertinoColors.transparent,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: widget.agreedToTerms ? ext.accent : ext.textHint,
width: 1.5,
),
),
child: widget.agreedToTerms
? Icon(
CupertinoIcons.checkmark,
size: 13,
color: ext.textOnAccent,
)
: null,
),
),
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text.rich(
TextSpan(
text: auth.registerAgreePrefix,
style: AppTypography.footnote.copyWith(color: ext.textSecondary),
children: [
WidgetSpan(
child: GestureDetector(
onTap: () => widget.onShowAgreement(true),
child: Text(
auth.userAgreement.startsWith('\u300a')
? auth.userAgreement
: '\u300a${auth.userAgreement}\u300b',
style: AppTypography.footnote.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
),
TextSpan(
text: auth.and,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
WidgetSpan(
child: GestureDetector(
onTap: () => widget.onShowAgreement(false),
child: Text(
auth.privacyPolicy.startsWith('\u300a')
? auth.privacyPolicy
: '\u300a${auth.privacyPolicy}\u300b',
style: AppTypography.footnote.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
],
);
}
}

View File

@@ -1,13 +1,12 @@
/// ============================================================
/// 闲言APP — 注册步骤3设置密码与安全选项
/// 创建时间: 2026-06-08
/// 更新时间: 2026-06-08
/// 作用: 注册流程第三步,设置密码、密保问题、订阅协议,完成注册
/// 上次更新: 从register_section.dart分流创建
/// 更新时间: 2026-06-12
/// 作用: 注册流程第三步,设置密码、密保问题、订阅邮件,完成注册
/// 上次更新: 移除协议勾选已移至步骤1
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_spacing.dart';
@@ -36,9 +35,6 @@ class RegisterStepPassword extends StatelessWidget {
required this.onSelectSecQuestion,
required this.subscribeEmail,
required this.onToggleSubscribe,
required this.agreedToTerms,
required this.onToggleAgreement,
required this.onShowAgreement,
required this.onPrev,
required this.onBack,
required this.onRegister,
@@ -63,9 +59,6 @@ class RegisterStepPassword extends StatelessWidget {
final VoidCallback onSelectSecQuestion;
final bool subscribeEmail;
final VoidCallback onToggleSubscribe;
final bool agreedToTerms;
final VoidCallback onToggleAgreement;
final void Function(bool isUserAgreement) onShowAgreement;
final VoidCallback onPrev;
final VoidCallback onBack;
final VoidCallback onRegister;
@@ -90,15 +83,30 @@ class RegisterStepPassword extends StatelessWidget {
),
child: Row(
children: [
Icon(CupertinoIcons.person_crop_circle_fill, size: 32, color: ext.accent),
Icon(
CupertinoIcons.person_crop_circle_fill,
size: 32,
color: ext.accent,
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(username, style: AppTypography.subhead.copyWith(color: ext.textPrimary, fontWeight: FontWeight.w600)),
Text(
username,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(email, style: AppTypography.caption1.copyWith(color: ext.textSecondary)),
Text(
email,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
),
@@ -124,7 +132,9 @@ class RegisterStepPassword extends StatelessWidget {
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
child: Center(
child: Icon(
obscurePassword ? CupertinoIcons.eye_slash : CupertinoIcons.eye,
obscurePassword
? CupertinoIcons.eye_slash
: CupertinoIcons.eye,
size: 18,
color: ext.textHint,
),
@@ -158,9 +168,6 @@ class RegisterStepPassword extends StatelessWidget {
// 邮件订阅
_buildSubscribeRow(),
const SizedBox(height: AppSpacing.lg),
// 协议
_buildAgreementRow(),
const SizedBox(height: AppSpacing.md),
// 导航按钮
_buildNavButtons(),
],
@@ -256,11 +263,7 @@ class RegisterStepPassword extends StatelessWidget {
),
),
),
Icon(
CupertinoIcons.chevron_right,
size: 14,
color: ext.textHint,
),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
);
@@ -276,9 +279,7 @@ class RegisterStepPassword extends StatelessWidget {
child: CupertinoTextField(
controller: secAnswerController,
placeholder: auth.enterSecAnswerHint,
placeholderStyle: AppTypography.body.copyWith(
color: ext.textHint,
),
placeholderStyle: AppTypography.body.copyWith(color: ext.textHint),
style: AppTypography.body.copyWith(color: ext.textPrimary),
decoration: null,
padding: const EdgeInsets.symmetric(
@@ -319,100 +320,13 @@ class RegisterStepPassword extends StatelessWidget {
const SizedBox(width: AppSpacing.sm),
Text(
auth.subscribeEmail,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
style: AppTypography.footnote.copyWith(color: ext.textSecondary),
),
],
),
);
}
/// 协议同意行
Widget _buildAgreementRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onToggleAgreement,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
child: Center(
child: Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(top: 1),
decoration: BoxDecoration(
color: agreedToTerms ? ext.accent : Colors.transparent,
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: agreedToTerms ? ext.accent : ext.textHint,
width: 1.5,
),
),
child: agreedToTerms
? Icon(
CupertinoIcons.checkmark,
size: 13,
color: ext.textOnAccent,
)
: null,
),
),
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text.rich(
TextSpan(
text: auth.registerAgreePrefix,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
children: [
WidgetSpan(
child: GestureDetector(
onTap: () => onShowAgreement(true),
child: Text(
auth.userAgreement.startsWith('\u300a')
? auth.userAgreement
: '\u300a${auth.userAgreement}\u300b',
style: AppTypography.footnote.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
),
TextSpan(
text: auth.and,
style: AppTypography.footnote.copyWith(
color: ext.textSecondary,
),
),
WidgetSpan(
child: GestureDetector(
onTap: () => onShowAgreement(false),
child: Text(
auth.privacyPolicy.startsWith('\u300a')
? auth.privacyPolicy
: '\u300a${auth.privacyPolicy}\u300b',
style: AppTypography.footnote.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
),
],
);
}
/// 导航按钮行(上一步 + 完成注册)
Widget _buildNavButtons() {
return Row(

View File

@@ -83,12 +83,21 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
Expanded(
child: SafeArea(
child: switch (state.currentView) {
RssView.subscriptions =>
_buildSubscriptionHome(ext, state, notifier),
RssView.articleList =>
_buildArticleList(ext, state, notifier),
RssView.articleDetail =>
_buildArticleDetail(ext, state, notifier),
RssView.subscriptions => _buildSubscriptionHome(
ext,
state,
notifier,
),
RssView.articleList => _buildArticleList(
ext,
state,
notifier,
),
RssView.articleDetail => _buildArticleDetail(
ext,
state,
notifier,
),
},
),
),
@@ -106,7 +115,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
RssView.subscriptions => '📡 RSS订阅',
};
Widget _buildLeading(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildLeading(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
if (state.currentView == RssView.articleDetail) {
return CupertinoButton(
padding: EdgeInsets.zero,
@@ -124,15 +137,25 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
return const AdaptiveBackButton();
}
Widget _buildTrailing(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildTrailing(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
// 文章详情 — 朗读按钮
if (state.currentView == RssView.articleDetail) {
return GestureDetector(
onTap: () => _speakArticle(state),
child: Container(
width: 32, height: 32,
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.mdBorder),
child: const Center(child: Text('🔊', style: TextStyle(fontSize: 14))),
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: const Center(
child: Text('🔊', style: TextStyle(fontSize: 14)),
),
),
);
}
@@ -144,13 +167,18 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
GestureDetector(
onTap: notifier.toggleCardMode,
child: Container(
width: 32, height: 32,
width: 32,
height: 32,
decoration: BoxDecoration(
color: state.isCardMode ? ext.accent.withValues(alpha: 0.12) : ext.bgSecondary,
color: state.isCardMode
? ext.accent.withValues(alpha: 0.12)
: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: Icon(
state.isCardMode ? CupertinoIcons.list_bullet : CupertinoIcons.square_grid_2x2,
state.isCardMode
? CupertinoIcons.list_bullet
: CupertinoIcons.square_grid_2x2,
size: 16,
color: state.isCardMode ? ext.accent : ext.textHint,
),
@@ -172,7 +200,8 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
GestureDetector(
onTap: _showAddSubscriptionSheet,
child: Container(
width: 32, height: 32,
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.mdBorder,
@@ -187,7 +216,10 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
/// 网络标识
Widget _buildNetworkBadge(AppThemeExtension ext) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.mdBorder,
@@ -206,15 +238,24 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
/// 网络异常提示条
Widget _buildNetworkWarningBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.xs),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.xs,
),
color: CupertinoColors.systemRed.withValues(alpha: 0.1),
child: Row(
children: [
const Icon(CupertinoIcons.wifi_exclamationmark, size: 14, color: CupertinoColors.systemRed),
const Icon(
CupertinoIcons.wifi_exclamationmark,
size: 14,
color: CupertinoColors.systemRed,
),
const SizedBox(width: AppSpacing.xs),
Text(
'网络连接异常,部分内容可能无法加载',
style: AppTypography.caption1.copyWith(color: CupertinoColors.systemRed),
style: AppTypography.caption1.copyWith(
color: CupertinoColors.systemRed,
),
),
],
),
@@ -223,14 +264,25 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// ── 订阅源首页(聚合视图) ──
Widget _buildSubscriptionHome(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildSubscriptionHome(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
final filteredSubs = _getFilteredSubscriptions(state);
return CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
sliver: SliverToBoxAdapter(child: _buildCategoryFilter(ext, state, notifier)),
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
0,
),
sliver: SliverToBoxAdapter(
child: _buildCategoryFilter(ext, state, notifier),
),
),
SliverPadding(
padding: const EdgeInsets.all(AppSpacing.md),
@@ -239,9 +291,18 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
const Text('📡', style: TextStyle(fontSize: 20)),
const SizedBox(width: AppSpacing.sm),
Text('我的订阅', style: AppTypography.headline.copyWith(color: ext.textPrimary, fontWeight: FontWeight.w600)),
Text(
'我的订阅',
style: AppTypography.headline.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: AppSpacing.sm),
Text('${filteredSubs.length}', style: AppTypography.caption1.copyWith(color: ext.textHint)),
Text(
'${filteredSubs.length}',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
@@ -270,30 +331,56 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
);
}
Widget _buildCategoryFilter(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildCategoryFilter(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return SizedBox(
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_buildCategoryChip(ext, label: '全部', isSelected: state.selectedCategory == null, onTap: () => notifier.setCategory(null)),
_buildCategoryChip(
ext,
label: '全部',
isSelected: state.selectedCategory == null,
onTap: () => notifier.setCategory(null),
),
const SizedBox(width: AppSpacing.xs),
...RssCategory.values.map((cat) => Padding(
padding: const EdgeInsets.only(right: AppSpacing.xs),
child: _buildCategoryChip(ext, label: cat.label, isSelected: state.selectedCategory == cat, onTap: () => notifier.setCategory(cat)),
)),
...RssCategory.values.map(
(cat) => Padding(
padding: const EdgeInsets.only(right: AppSpacing.xs),
child: _buildCategoryChip(
ext,
label: cat.label,
isSelected: state.selectedCategory == cat,
onTap: () => notifier.setCategory(cat),
),
),
),
],
),
);
}
Widget _buildCategoryChip(AppThemeExtension ext, {required String label, required bool isSelected, required VoidCallback onTap}) {
Widget _buildCategoryChip(
AppThemeExtension ext, {
required String label,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm + 2, vertical: AppSpacing.xs + 1),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm + 2,
vertical: AppSpacing.xs + 1,
),
decoration: BoxDecoration(
color: isSelected ? ext.accent.withValues(alpha: 0.12) : ext.bgSecondary,
color: isSelected
? ext.accent.withValues(alpha: 0.12)
: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
border: isSelected ? Border.all(color: ext.accent) : null,
),
@@ -315,9 +402,15 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
const Text('📡', style: TextStyle(fontSize: 56)),
const SizedBox(height: AppSpacing.md),
Text('暂无订阅源', style: AppTypography.headline.copyWith(color: ext.textSecondary)),
Text(
'暂无订阅源',
style: AppTypography.headline.copyWith(color: ext.textSecondary),
),
const SizedBox(height: AppSpacing.sm),
Text('点击右上角 + 添加RSS订阅源', style: AppTypography.subhead.copyWith(color: ext.textHint)),
Text(
'点击右上角 + 添加RSS订阅源',
style: AppTypography.subhead.copyWith(color: ext.textHint),
),
const SizedBox(height: AppSpacing.lg),
SizedBox(
width: 200,
@@ -325,7 +418,13 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
color: ext.accent,
borderRadius: AppRadius.lgBorder,
onPressed: _showAddSubscriptionSheet,
child: Text(' 添加订阅', style: AppTypography.subhead.copyWith(color: ext.textOnAccent, fontWeight: FontWeight.w600)),
child: Text(
' 添加订阅',
style: AppTypography.subhead.copyWith(
color: ext.textOnAccent,
fontWeight: FontWeight.w600,
),
),
),
),
],
@@ -343,7 +442,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// ── 文章列表 ──
Widget _buildArticleList(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildArticleList(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return Column(
children: [
// 搜索栏
@@ -351,16 +454,16 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// 文章内容区域
Expanded(
child: state.isLoading
? _buildShimmerList()
: state.errorMessage != null
? _buildShimmerList()
: state.errorMessage != null
? _buildErrorState(ext, notifier)
: state.searchQuery.isNotEmpty
? _buildSearchResults(ext, state, notifier)
: state.feedItems.isEmpty
? _buildEmptyArticleState(ext, notifier)
: state.isCardMode
? _buildCardModeView(ext, state, notifier)
: _buildArticleListView(ext, state, notifier),
? _buildSearchResults(ext, state, notifier)
: state.feedItems.isEmpty
? _buildEmptyArticleState(ext, notifier)
: state.isCardMode
? _buildCardModeView(ext, state, notifier)
: _buildArticleListView(ext, state, notifier),
),
],
);
@@ -371,7 +474,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
return ListView.builder(
itemCount: 5,
itemBuilder: (_, __) => Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm, left: AppSpacing.md, right: AppSpacing.md),
padding: const EdgeInsets.only(
bottom: AppSpacing.sm,
left: AppSpacing.md,
right: AppSpacing.md,
),
child: ShimmerPlaceholder.card(),
),
);
@@ -385,7 +492,10 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
const Text('📭', style: TextStyle(fontSize: 48)),
const SizedBox(height: AppSpacing.md),
Text('暂无文章', style: AppTypography.subhead.copyWith(color: ext.textHint)),
Text(
'暂无文章',
style: AppTypography.subhead.copyWith(color: ext.textHint),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
onPressed: () => notifier.loadFeed(refresh: true),
@@ -397,7 +507,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
}
/// 文章列表视图(非卡片模式)
Widget _buildArticleListView(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildArticleListView(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return RefreshIndicator(
color: ext.accent,
onRefresh: () => notifier.loadFeed(refresh: true),
@@ -405,11 +519,18 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.only(left: AppSpacing.md, right: AppSpacing.md, top: AppSpacing.sm),
padding: const EdgeInsets.only(
left: AppSpacing.md,
right: AppSpacing.md,
top: AppSpacing.sm,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
final item = state.feedItems[index];
return RssArticleCard(item: item, onTap: () => notifier.openDetail(item));
return RssArticleCard(
item: item,
onTap: () => notifier.openDetail(item),
);
}, childCount: state.feedItems.length),
),
),
@@ -441,13 +562,23 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
const Text('😔', style: TextStyle(fontSize: 48)),
const SizedBox(height: AppSpacing.md),
Text(state.errorMessage ?? '加载失败', style: AppTypography.subhead.copyWith(color: ext.textHint), textAlign: TextAlign.center),
Text(
state.errorMessage ?? '加载失败',
style: AppTypography.subhead.copyWith(color: ext.textHint),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
color: ext.accent,
borderRadius: AppRadius.lgBorder,
onPressed: () => notifier.loadFeed(refresh: true),
child: Text('🔄 重试', style: AppTypography.subhead.copyWith(color: ext.textOnAccent, fontWeight: FontWeight.w600)),
child: Text(
'🔄 重试',
style: AppTypography.subhead.copyWith(
color: ext.textOnAccent,
fontWeight: FontWeight.w600,
),
),
),
],
),
@@ -457,15 +588,22 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// ── 文章详情 ──
Widget _buildArticleDetail(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildArticleDetail(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
final item = state.selectedItem;
if (item == null) return const SizedBox.shrink();
final isReadingMode = state.fullTextResult != null && state.fullTextResult!.success;
final isReadingMode =
state.fullTextResult != null && state.fullTextResult!.success;
final isBookmarked = state.bookmarkedUids.contains(item.uid);
// 获取已保存的阅读进度
final savedProgress = notifier.getReadingProgress(item.uid);
final scrollController = ScrollController(initialScrollOffset: savedProgress);
final scrollController = ScrollController(
initialScrollOffset: savedProgress,
);
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
@@ -492,11 +630,18 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
// 标题
Text(
isReadingMode ? (state.fullTextResult!.title ?? item.title) : item.title,
style: AppTypography.title2.copyWith(color: ext.textPrimary, fontWeight: FontWeight.bold),
isReadingMode
? (state.fullTextResult!.title ?? item.title)
: item.title,
style: AppTypography.title2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.bold,
),
),
// 元信息
if (item.sourceTitle != null || item.author != null || item.pubDate != null) ...[
if (item.sourceTitle != null ||
item.author != null ||
item.pubDate != null) ...[
const SizedBox(height: AppSpacing.sm),
_buildMetaRow(ext, item),
],
@@ -506,17 +651,37 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
const Center(child: CupertinoActivityIndicator()),
const SizedBox(height: AppSpacing.md),
] else if (isReadingMode) ...[
Text(state.fullTextResult!.content ?? '', style: AppTypography.body.copyWith(color: ext.textPrimary, height: 1.8)),
Text(
state.fullTextResult!.content ?? '',
style: AppTypography.body.copyWith(
color: ext.textPrimary,
height: 1.8,
),
),
if (state.fullTextResult!.images.isNotEmpty) ...[
const SizedBox(height: AppSpacing.lg),
_buildImageGallery(ext, state.fullTextResult!.images),
],
] else if (item.description != null && item.description!.isNotEmpty) ...[
Text(HtmlUtils.stripTags(item.description!), style: AppTypography.body.copyWith(color: ext.textSecondary, height: 1.7)),
] else if (item.description != null &&
item.description!.isNotEmpty) ...[
Text(
HtmlUtils.stripTags(item.description!),
style: AppTypography.body.copyWith(
color: ext.textSecondary,
height: 1.7,
),
),
],
const SizedBox(height: AppSpacing.lg),
// 操作按钮
if (item.link != null) _buildActionButtons(ext, notifier, item, isReadingMode, isBookmarked),
if (item.link != null)
_buildActionButtons(
ext,
notifier,
item,
isReadingMode,
isBookmarked,
),
],
),
),
@@ -534,10 +699,16 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
child: ClipRRect(
borderRadius: AppRadius.lgBorder,
child: CachedNetworkImage(
imageUrl: imageUrl, width: double.infinity, height: 200, fit: BoxFit.cover,
imageUrl: imageUrl,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
height: 200,
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.lgBorder),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.lgBorder,
),
child: const Center(child: CupertinoActivityIndicator()),
),
errorWidget: (_, __, ___) => const SizedBox.shrink(),
@@ -547,7 +718,13 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
}
/// 操作按钮区域(查看原文 + 收藏 + 阅读模式)
Widget _buildActionButtons(AppThemeExtension ext, RssNotifier notifier, RssFeedItem item, bool isReadingMode, bool isBookmarked) {
Widget _buildActionButtons(
AppThemeExtension ext,
RssNotifier notifier,
RssFeedItem item,
bool isReadingMode,
bool isBookmarked,
) {
return Column(
children: [
Row(
@@ -561,9 +738,19 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(CupertinoIcons.doc_text_search, size: 16, color: ext.textOnAccent),
Icon(
CupertinoIcons.doc_text_search,
size: 16,
color: ext.textOnAccent,
),
const SizedBox(width: AppSpacing.xs),
Text('查看原文', style: AppTypography.subhead.copyWith(color: ext.textOnAccent, fontWeight: FontWeight.w600)),
Text(
'查看原文',
style: AppTypography.subhead.copyWith(
color: ext.textOnAccent,
fontWeight: FontWeight.w600,
),
),
],
),
),
@@ -571,7 +758,10 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
const SizedBox(width: AppSpacing.sm),
// 收藏按钮
CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.lgBorder,
onPressed: () {
@@ -581,9 +771,21 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(isBookmarked ? CupertinoIcons.bookmark_fill : CupertinoIcons.bookmark, size: 16, color: ext.accent),
Icon(
isBookmarked
? CupertinoIcons.bookmark_fill
: CupertinoIcons.bookmark,
size: 16,
color: ext.accent,
),
const SizedBox(width: AppSpacing.xs),
Text(isBookmarked ? '已收藏' : '收藏', style: AppTypography.subhead.copyWith(color: ext.accent, fontWeight: FontWeight.w600)),
Text(
isBookmarked ? '已收藏' : '收藏',
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
),
@@ -595,7 +797,10 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
SizedBox(
width: double.infinity,
child: CupertinoButton(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md, vertical: AppSpacing.sm),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.lgBorder,
onPressed: notifier.loadFullText,
@@ -604,7 +809,13 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
children: [
Icon(CupertinoIcons.doc_text, size: 16, color: ext.accent),
const SizedBox(width: AppSpacing.xs),
Text('📖 阅读模式', style: AppTypography.subhead.copyWith(color: ext.accent, fontWeight: FontWeight.w600)),
Text(
'📖 阅读模式',
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w600,
),
),
],
),
),
@@ -622,10 +833,16 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
title: const Text('查看原文'),
content: const Text('即将跳转到外部浏览器打开原文链接,是否继续?'),
actions: [
CupertinoDialogAction(onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')),
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () { Navigator.of(ctx).pop(); _launchUrl(url); },
onPressed: () {
Navigator.of(ctx).pop();
_launchUrl(url);
},
child: const Text('继续'),
),
],
@@ -638,26 +855,47 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('📎 文中图片', style: AppTypography.subhead.copyWith(color: ext.textSecondary, fontWeight: FontWeight.w600)),
Text(
'📎 文中图片',
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppSpacing.sm),
Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: images.map((url) => GestureDetector(
onTap: () => Navigator.of(context).push(CupertinoPageRoute<void>(builder: (_) => FullScreenPhotoView(imageUrl: url))),
child: ClipRRect(
borderRadius: AppRadius.mdBorder,
child: CachedNetworkImage(
imageUrl: url, width: 100, height: 100, fit: BoxFit.cover,
placeholder: (_, __) => Container(
width: 100, height: 100,
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.mdBorder),
child: const CupertinoActivityIndicator(radius: 8),
children: images
.map(
(url) => GestureDetector(
onTap: () => Navigator.of(context).push(
CupertinoPageRoute<void>(
builder: (_) => FullScreenPhotoView(imageUrl: url),
),
),
child: ClipRRect(
borderRadius: AppRadius.mdBorder,
child: CachedNetworkImage(
imageUrl: url,
width: 100,
height: 100,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
),
child: const CupertinoActivityIndicator(radius: 8),
),
errorWidget: (_, __, ___) => const SizedBox.shrink(),
),
),
),
errorWidget: (_, __, ___) => const SizedBox.shrink(),
),
),
)).toList(),
)
.toList(),
),
],
);
@@ -666,26 +904,52 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
/// 文章元信息行
Widget _buildMetaRow(AppThemeExtension ext, RssFeedItem item) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: AppSpacing.xs),
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.smBorder),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.smBorder,
),
child: Row(
children: [
if (item.sourceTitle != null) ...[
Icon(CupertinoIcons.antenna_radiowaves_left_right, size: 12, color: ext.accent),
Icon(
CupertinoIcons.antenna_radiowaves_left_right,
size: 12,
color: ext.accent,
),
const SizedBox(width: 3),
Text(item.sourceTitle!, style: AppTypography.caption2.copyWith(color: ext.accent, fontWeight: FontWeight.w500)),
Text(
item.sourceTitle!,
style: AppTypography.caption2.copyWith(
color: ext.accent,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: AppSpacing.sm),
],
if (item.author != null) ...[
Icon(CupertinoIcons.person, size: 12, color: ext.textHint),
const SizedBox(width: 3),
Flexible(child: Text(item.author!, style: AppTypography.caption2.copyWith(color: ext.textHint), maxLines: 1, overflow: TextOverflow.ellipsis)),
Flexible(
child: Text(
item.author!,
style: AppTypography.caption2.copyWith(color: ext.textHint),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: AppSpacing.sm),
],
if (item.pubDate != null) ...[
Icon(CupertinoIcons.time, size: 12, color: ext.textHint),
const SizedBox(width: 3),
Text(item.pubDate!.timeAgo, style: AppTypography.caption2.copyWith(color: ext.textHint)),
Text(
item.pubDate!.timeAgo,
style: AppTypography.caption2.copyWith(color: ext.textHint),
),
],
],
),
@@ -717,20 +981,29 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
} else {
text = '${item.title}\n\n${HtmlUtils.stripTags(item.description ?? '')}';
}
if (text.trim().isEmpty) { _showToast('暂无内容可朗读'); return; }
if (text.trim().isEmpty) {
_showToast('暂无内容可朗读');
return;
}
TtsPlayerSheet.show(context, text: text);
}
// ── 删除订阅 ──
Future<void> _deleteSubscription(RssSubscription sub, RssNotifier notifier) async {
Future<void> _deleteSubscription(
RssSubscription sub,
RssNotifier notifier,
) async {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: const Text('删除订阅源'),
content: Text('确定要删除「${sub.title}」吗?'),
actions: [
CupertinoDialogAction(onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消')),
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () async {
@@ -752,7 +1025,8 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
return GestureDetector(
onTap: () => _showMoreActions(ext, notifier),
child: Container(
width: 32, height: 32,
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.bgSecondary,
borderRadius: AppRadius.mdBorder,
@@ -769,11 +1043,17 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
builder: (ctx) => CupertinoActionSheet(
actions: [
CupertinoActionSheetAction(
onPressed: () { Navigator.of(ctx).pop(); _exportOpml(notifier); },
onPressed: () {
Navigator.of(ctx).pop();
_exportOpml(notifier);
},
child: Text('📤 导出OPML', style: TextStyle(color: ext.accent)),
),
CupertinoActionSheetAction(
onPressed: () { Navigator.of(ctx).pop(); _importOpml(notifier); },
onPressed: () {
Navigator.of(ctx).pop();
_importOpml(notifier);
},
child: Text('📥 导入OPML', style: TextStyle(color: ext.accent)),
),
],
@@ -792,10 +1072,9 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/rss_subscriptions.opml');
await file.writeAsString(opml);
await SharePlus.instance.share(ShareParams(
text: 'RSS订阅源导出',
files: [XFile(file.path)],
));
await SharePlus.instance.share(
ShareParams(text: 'RSS订阅源导出', files: [XFile(file.path)]),
);
} catch (e) {
Log.e('RssReader', 'OPML导出失败: $e');
_showToast('❌ 导出失败');
@@ -815,8 +1094,12 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
if (file.path != null) {
opmlXml = await File(file.path!).readAsString();
} else {
// 降级:使用readAsBytes读取
final bytes = await file.readAsBytes();
// 降级:使用 bytes 属性读取file_picker 11.x API
final bytes = file.bytes;
if (bytes == null || bytes.isEmpty) {
_showToast('❌ 无法读取文件数据');
return;
}
opmlXml = utf8.decode(bytes);
}
final count = await notifier.importOpml(opmlXml);
@@ -834,7 +1117,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// ── 文章搜索 ──
/// 搜索栏使用RssSearchBar组件
Widget _buildSearchBar(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildSearchBar(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return RssSearchBar(
searchQuery: state.searchQuery,
onChanged: (query) => notifier.searchArticles(query),
@@ -843,7 +1130,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
}
/// 搜索结果列表使用RssSearchResultsList组件
Widget _buildSearchResults(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildSearchResults(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return RssSearchResultsList(
searchResults: state.searchResults,
isSearching: state.isSearching,
@@ -860,7 +1151,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
// ── 卡片式阅读模式 ──
/// 卡片阅读模式视图使用RssCardModeView组件
Widget _buildCardModeView(AppThemeExtension ext, RssState state, RssNotifier notifier) {
Widget _buildCardModeView(
AppThemeExtension ext,
RssState state,
RssNotifier notifier,
) {
return RssCardModeView(
feedItems: state.feedItems,
bookmarkedUids: state.bookmarkedUids,
@@ -880,7 +1175,8 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
Future<void> _launchUrl(String url) async {
try {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) await launchUrl(uri, mode: LaunchMode.externalApplication);
if (await canLaunchUrl(uri))
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {
Log.e('RssReader', '打开链接失败: $e');
}
@@ -890,7 +1186,11 @@ class _RssReaderPageState extends ConsumerState<RssReaderPage> {
if (!mounted) return;
HapticFeedback.lightImpact();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating),
SnackBar(
content: Text(message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
}

View File

@@ -662,9 +662,7 @@ class FontManagementNotifier extends Notifier<FontManagementState>
return;
}
try {
final result = await FilePicker.pickFiles(
dialogTitle: '选择ZIP字体包',
);
final result = await FilePicker.pickFiles(dialogTitle: '选择ZIP字体包');
if (result == null || result.files.isEmpty) return;
final platformFile = result.files.first;
@@ -684,7 +682,8 @@ class FontManagementNotifier extends Notifier<FontManagementState>
zipBytes = await zipFile.readAsBytes();
}
}
// 降级使用readAsBytes读取
// final bytes = await file.readAsBytes();
// 路径不可用时,使用 bytes 属性读取file_picker 11.x API
if (zipBytes == null) {
final rawBytes = platformFile.bytes;