鸿蒙提交
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user