MacBook端提交
This commit is contained in:
44
CHANGELOG.md
44
CHANGELOG.md
@@ -4,6 +4,50 @@
|
||||
|
||||
***
|
||||
|
||||
## [v6.20.19] - 2026-06-07
|
||||
|
||||
### 🐛 修复 — 句子广场选择分类后仍显示混合句子
|
||||
|
||||
#### 问题
|
||||
选择具体分类(如"古诗词")后,句子列表仍显示"推荐"模式的混合数据,而非所选分类的句子。
|
||||
|
||||
#### 原因
|
||||
1. `selectType()` 切换分类时未清空旧句子列表,新数据加载期间仍显示旧的混合数据
|
||||
2. `fetchNewSentences()` 和 `fetchRefreshSentences()` 中的分类过滤逻辑仅检查"是否启用",未严格匹配当前选中的分类
|
||||
|
||||
#### 修复
|
||||
- `selectType()` 无缓存时清空 `sentences` 并设置 `isForceLoading=true`,显示骨架屏而非旧数据
|
||||
- `fetchNewSentences()` 和 `fetchRefreshSentences()` 增加严格分类过滤:选中具体分类时只保留该分类数据
|
||||
- fallback 逻辑同步修复
|
||||
|
||||
#### 修改文件
|
||||
- `lib/features/home/providers/home_provider.dart` — selectType 清空旧数据
|
||||
- `lib/features/home/providers/home_feed_mixin.dart` — 严格分类过滤
|
||||
|
||||
***
|
||||
|
||||
## [v6.20.18] - 2026-06-07
|
||||
|
||||
### 📊 改进 — 句子广场/句子来源分类由后台推荐权重管理控制
|
||||
|
||||
#### 服务端修改
|
||||
- `/api/feed/channels` 接口根据 `tool_feed_weight_config` 表的 `is_enabled` 字段过滤分类,只返回后台启用的分类
|
||||
- `/api/feed/stats` 接口同步过滤,统计数据只包含启用分类
|
||||
- 频道列表响应新增 `is_enabled` 字段
|
||||
- 未在权重配置表中的类型默认启用(保持兼容)
|
||||
|
||||
#### 前端修改
|
||||
- `FeedChannel` 模型新增 `isEnabled` 字段,解析服务端 `is_enabled`
|
||||
- `source_provider.dart` 移除硬编码的 `_kDefaultDisabledKeys`(原默认禁用 jieqi/article/lunyu/abbr/jiufang),改为由服务端控制
|
||||
- 句子来源页面分类总数现在正确反映后台开启的分类数量
|
||||
|
||||
#### 修改文件
|
||||
- `docs/toolsapi/application/api/controller/Feed.php` — channels/stats 方法增加 is_enabled 过滤
|
||||
- `lib/features/home/models/feed_model.dart` — FeedChannel 添加 isEnabled 字段
|
||||
- `lib/features/source/providers/source_provider.dart` — 移除硬编码禁用列表
|
||||
|
||||
***
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
## [v6.20.17] - 2026-06-07
|
||||
|
||||
|
||||
@@ -658,16 +658,31 @@ class Feed extends Api
|
||||
|
||||
public function channels()
|
||||
{
|
||||
// 获取权重配置,用于过滤启用状态
|
||||
$weightConfig = $this->_getWeightConfig();
|
||||
|
||||
$channels = [];
|
||||
$channels[] = ['key' => 'all', 'name' => '推荐', 'icon' => '🔥', 'count' => 0];
|
||||
$channels[] = ['key' => 'all', 'name' => '推荐', 'icon' => '🔥', 'count' => 0, 'is_enabled' => true];
|
||||
|
||||
foreach (self::$feedMap as $key => $config) {
|
||||
// 检查权重配置中的启用状态
|
||||
$isEnabled = true; // 未配置的类型默认启用
|
||||
if (isset($weightConfig[$key])) {
|
||||
$isEnabled = !empty($weightConfig[$key]['is_enabled']);
|
||||
}
|
||||
|
||||
// 只返回后台启用的分类
|
||||
if (!$isEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $this->_countFeedItems($key);
|
||||
$channels[] = [
|
||||
'key' => $key,
|
||||
'name' => $config['name'],
|
||||
'icon' => $config['icon'],
|
||||
'count' => $count,
|
||||
'is_enabled' => true,
|
||||
];
|
||||
$channels[0]['count'] += $count;
|
||||
}
|
||||
@@ -1543,11 +1558,27 @@ class Feed extends Api
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取权重配置,用于过滤启用状态
|
||||
$weightConfig = $this->_getWeightConfig();
|
||||
|
||||
$channelStats = [];
|
||||
$totalContent = 0;
|
||||
$totalViews = 0;
|
||||
$enabledCount = 0;
|
||||
|
||||
foreach (self::$feedMap as $key => $config) {
|
||||
// 检查权重配置中的启用状态
|
||||
$isEnabled = true;
|
||||
if (isset($weightConfig[$key])) {
|
||||
$isEnabled = !empty($weightConfig[$key]['is_enabled']);
|
||||
}
|
||||
|
||||
// 只返回后台启用的分类统计
|
||||
if (!$isEnabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$enabledCount++;
|
||||
$count = $this->_countFeedItems($key);
|
||||
$views = 0;
|
||||
try {
|
||||
@@ -1578,7 +1609,7 @@ class Feed extends Api
|
||||
$result = [
|
||||
'total_content' => $totalContent,
|
||||
'total_views' => $totalViews,
|
||||
'channel_count' => count(self::$feedMap),
|
||||
'channel_count' => $enabledCount,
|
||||
'channels' => $channelStats,
|
||||
'interactions' => $interactionStats,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
|
||||
@@ -202,34 +202,32 @@ class _DailyCardArViewState extends ConsumerState<DailyCardArView>
|
||||
|
||||
// ---- 手势处理 ----
|
||||
|
||||
void _onPanStart(DragStartDetails details) {
|
||||
_lastPanPosition = details.localPosition;
|
||||
void _onScaleStart(ScaleStartDetails details) {
|
||||
_lastPanPosition = details.localFocalPoint;
|
||||
}
|
||||
|
||||
void _onPanUpdate(DragUpdateDetails details) {
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
// 缩放处理
|
||||
setState(() {
|
||||
_scale = (_scale * details.scale).clamp(0.6, 2.0);
|
||||
});
|
||||
// 旋转处理(原 pan 逻辑)
|
||||
if (_lastPanPosition == null) return;
|
||||
final delta = details.localPosition - _lastPanPosition!;
|
||||
final delta = details.localFocalPoint - _lastPanPosition!;
|
||||
final size = MediaQuery.of(context).size;
|
||||
setState(() {
|
||||
_rotateY += (delta.dx / size.width) * _maxGestureAngle;
|
||||
_rotateX -= (delta.dy / size.height) * _maxGestureAngle;
|
||||
// 限制旋转范围
|
||||
_rotateX = _rotateX.clamp(-_maxGestureAngle, _maxGestureAngle);
|
||||
_rotateY = _rotateY.clamp(-_maxGestureAngle, _maxGestureAngle);
|
||||
});
|
||||
_lastPanPosition = details.localPosition;
|
||||
_lastPanPosition = details.localFocalPoint;
|
||||
}
|
||||
|
||||
void _onPanEnd(DragEndDetails details) {
|
||||
void _onScaleEnd(ScaleEndDetails details) {
|
||||
_lastPanPosition = null;
|
||||
}
|
||||
|
||||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
setState(() {
|
||||
_scale = (_scale * details.scale).clamp(0.6, 2.0);
|
||||
});
|
||||
}
|
||||
|
||||
void _onDoubleTap() {
|
||||
HapticFeedback.lightImpact();
|
||||
setState(() {
|
||||
@@ -411,11 +409,10 @@ class _DailyCardArViewState extends ConsumerState<DailyCardArView>
|
||||
final totalRotateY = _tiltY + _rotateY;
|
||||
|
||||
return GestureDetector(
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
onDoubleTap: _onDoubleTap,
|
||||
onScaleStart: _onScaleStart,
|
||||
onScaleUpdate: _onScaleUpdate,
|
||||
onScaleEnd: _onScaleEnd,
|
||||
onDoubleTap: _onDoubleTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
|
||||
@@ -948,6 +948,7 @@ const defaultTools = <ToolItem>[
|
||||
presetKeywords: ['天气', '农事', '健康', '学习', '做人'],
|
||||
),
|
||||
),
|
||||
// TODO: 歌词大全 - 暂时隐藏,待内容完善后恢复
|
||||
ToolItem(
|
||||
id: 'lyric',
|
||||
name: '歌词大全',
|
||||
@@ -957,6 +958,7 @@ const defaultTools = <ToolItem>[
|
||||
route: '/tool/lyric',
|
||||
description: '歌词搜索',
|
||||
apiPath: '/api/hanzi/search',
|
||||
isDeleted: true,
|
||||
navConfig: ToolNavConfig.list(
|
||||
toolId: 'lyric',
|
||||
title: '歌词大全',
|
||||
@@ -967,6 +969,7 @@ const defaultTools = <ToolItem>[
|
||||
presetKeywords: ['月亮', '春天', '爱情', '故乡', '朋友'],
|
||||
),
|
||||
),
|
||||
// TODO: 故事大全 - 暂时隐藏,待内容完善后恢复
|
||||
ToolItem(
|
||||
id: 'story',
|
||||
name: '故事大全',
|
||||
@@ -976,6 +979,7 @@ const defaultTools = <ToolItem>[
|
||||
route: '/tool/story',
|
||||
description: '故事搜索阅读',
|
||||
apiPath: '/api/hanzi/search',
|
||||
isDeleted: true,
|
||||
navConfig: ToolNavConfig.list(
|
||||
toolId: 'story',
|
||||
title: '故事大全',
|
||||
@@ -986,6 +990,7 @@ const defaultTools = <ToolItem>[
|
||||
presetKeywords: ['公主', '王子', '龙', '魔法', '冒险'],
|
||||
),
|
||||
),
|
||||
// TODO: 作文大全 - 暂时隐藏,待内容完善后恢复
|
||||
ToolItem(
|
||||
id: 'zuowen',
|
||||
name: '作文大全',
|
||||
@@ -995,6 +1000,7 @@ const defaultTools = <ToolItem>[
|
||||
route: '/tool/zuowen',
|
||||
description: '优秀作文参考',
|
||||
apiPath: '/api/hanzi/search',
|
||||
isDeleted: true,
|
||||
navConfig: ToolNavConfig.list(
|
||||
toolId: 'zuowen',
|
||||
title: '作文大全',
|
||||
|
||||
@@ -419,8 +419,11 @@ class ToolCenterNotifier extends Notifier<ToolCenterState> {
|
||||
KvStorage.getBool('${_keyPrefix}pin_${tool.id}') ?? false;
|
||||
final isFavorited =
|
||||
KvStorage.getBool('${_keyPrefix}fav_${tool.id}') ?? false;
|
||||
final isDeleted =
|
||||
KvStorage.getBool('${_keyPrefix}del_${tool.id}') ?? false;
|
||||
// 代码中 isDeleted=true 表示强制隐藏(如歌词大全/故事大全/作文大全),
|
||||
// 持久化数据不应覆盖强制隐藏状态
|
||||
final persistedDeleted =
|
||||
KvStorage.getBool('${_keyPrefix}del_${tool.id}');
|
||||
final isDeleted = tool.isDeleted || (persistedDeleted ?? false);
|
||||
final rating =
|
||||
KvStorage.getDouble('${_keyPrefix}rating_${tool.id}') ?? 0.0;
|
||||
final ratingCount =
|
||||
|
||||
@@ -382,6 +382,7 @@ class FeedChannel {
|
||||
required this.name,
|
||||
required this.icon,
|
||||
required this.count,
|
||||
this.isEnabled = true,
|
||||
});
|
||||
|
||||
final String key;
|
||||
@@ -389,17 +390,27 @@ class FeedChannel {
|
||||
final String icon;
|
||||
final int count;
|
||||
|
||||
/// 后台启用状态(来自推荐权重管理配置)
|
||||
final bool isEnabled;
|
||||
|
||||
factory FeedChannel.fromJson(Map<String, dynamic> json) {
|
||||
return FeedChannel(
|
||||
key: json['key'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
icon: json['icon'] as String? ?? '',
|
||||
count: _parseInt(json['count']),
|
||||
isEnabled: json['is_enabled'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'key': key, 'name': name, 'icon': icon, 'count': count};
|
||||
return {
|
||||
'key': key,
|
||||
'name': name,
|
||||
'icon': icon,
|
||||
'count': count,
|
||||
'is_enabled': isEnabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -468,6 +468,8 @@ class _HomePageState extends ConsumerState<HomePage> {
|
||||
onMarkRead: (id) =>
|
||||
ref.read(homeProvider.notifier).markRead(id),
|
||||
onRefresh: () => ref.read(homeProvider.notifier).refresh(),
|
||||
onRefreshSentenceList: () =>
|
||||
ref.read(homeProvider.notifier).refreshSentenceList(),
|
||||
onSlidableOpened: _gestureController.onSlidableOpened,
|
||||
onSlidableClosed: _gestureController.onSlidableClosed,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 首页句子列表区域
|
||||
/// 创建时间: 2026-05-12
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 首页句子广场的句子列表渲染,包含骨架屏、空状态、倒计时自动刷新
|
||||
/// 上次更新: 新增分类切换倒计时tips、刷新按钮主题适配、onRefreshSentenceList回调
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -33,6 +41,7 @@ class HomeSentenceListSection extends ConsumerStatefulWidget {
|
||||
required this.onToggleReadLater,
|
||||
required this.onMarkRead,
|
||||
required this.onRefresh,
|
||||
required this.onRefreshSentenceList,
|
||||
required this.onSlidableOpened,
|
||||
required this.onSlidableClosed,
|
||||
super.key,
|
||||
@@ -47,6 +56,8 @@ class HomeSentenceListSection extends ConsumerStatefulWidget {
|
||||
final void Function(String id) onToggleReadLater;
|
||||
final void Function(String id) onMarkRead;
|
||||
final VoidCallback onRefresh;
|
||||
/// 仅刷新句子广场列表,不刷新每日句子卡片
|
||||
final VoidCallback onRefreshSentenceList;
|
||||
final VoidCallback onSlidableOpened;
|
||||
final VoidCallback onSlidableClosed;
|
||||
|
||||
@@ -55,7 +66,81 @@ class HomeSentenceListSection extends ConsumerStatefulWidget {
|
||||
_HomeSentenceListSectionState();
|
||||
}
|
||||
|
||||
class _HomeSentenceListSectionState extends ConsumerState<HomeSentenceListSection> {
|
||||
class _HomeSentenceListSectionState extends ConsumerState<HomeSentenceListSection>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Timer? _autoRefreshTimer;
|
||||
int _countdown = 3;
|
||||
bool _showCountdown = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkCategorySwitching();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant HomeSentenceListSection oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_checkCategorySwitching();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 检测分类切换后的空状态,启动倒计时自动刷新
|
||||
void _checkCategorySwitching() {
|
||||
final state = widget.state;
|
||||
// 数据到达后隐藏倒计时
|
||||
if (state.sentences.isNotEmpty) {
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = null;
|
||||
if (_showCountdown) {
|
||||
setState(() {
|
||||
_showCountdown = false;
|
||||
_countdown = 3;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 分类切换后数据为空:启动倒计时
|
||||
if (state.isCategorySwitching &&
|
||||
!state.isForceLoading &&
|
||||
!state.isLoading &&
|
||||
state.sentences.isEmpty &&
|
||||
!_showCountdown) {
|
||||
setState(() {
|
||||
_showCountdown = true;
|
||||
_countdown = 3;
|
||||
});
|
||||
_startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
void _startCountdown() {
|
||||
_autoRefreshTimer?.cancel();
|
||||
_autoRefreshTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_countdown--;
|
||||
});
|
||||
if (_countdown <= 0) {
|
||||
timer.cancel();
|
||||
setState(() {
|
||||
_showCountdown = false;
|
||||
_countdown = 3;
|
||||
});
|
||||
// 自动刷新(仅句子列表)
|
||||
widget.onRefreshSentenceList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
@@ -75,10 +160,63 @@ class _HomeSentenceListSectionState extends ConsumerState<HomeSentenceListSectio
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isForceLoading && state.sentences.isEmpty) {
|
||||
if (state.isForceLoading) {
|
||||
return const SliverToBoxAdapter(child: FeedSentenceSkeleton());
|
||||
}
|
||||
|
||||
// 分类切换后的空状态:显示倒计时tips
|
||||
if (_showCountdown && state.sentences.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Lottie.asset(
|
||||
'assets/animations/star.json',
|
||||
width: 120,
|
||||
height: 120,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Text('⏳', style: TextStyle(fontSize: 48)),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
t.home.base.loadingContent,
|
||||
style: AppTypography.headline.copyWith(
|
||||
color: ext.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
t.home.base.autoRefreshCountdown(_countdown),
|
||||
style: AppTypography.subhead.copyWith(color: ext.textHint),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
CupertinoButton(
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed: () {
|
||||
_autoRefreshTimer?.cancel();
|
||||
setState(() {
|
||||
_showCountdown = false;
|
||||
_countdown = 3;
|
||||
});
|
||||
widget.onRefreshSentenceList();
|
||||
},
|
||||
child: Text(
|
||||
t.common.refresh,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.accent.computeLuminance() > 0.5
|
||||
? CupertinoColors.black
|
||||
: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!state.isLoading && !state.isForceLoading && state.sentences.isEmpty) {
|
||||
return SliverFillRemaining(
|
||||
child: Center(
|
||||
@@ -109,7 +247,14 @@ class _HomeSentenceListSectionState extends ConsumerState<HomeSentenceListSectio
|
||||
color: ext.accent,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
onPressed: widget.onRefresh,
|
||||
child: Text(t.common.refresh),
|
||||
child: Text(
|
||||
t.common.refresh,
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.accent.computeLuminance() > 0.5
|
||||
? CupertinoColors.black
|
||||
: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -84,13 +84,22 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
}
|
||||
|
||||
final enabledChannelKeys = state.channels.map((c) => c.key).toSet();
|
||||
// 当选择了具体分类时,只保留该分类的数据
|
||||
final selectedType = state.selectedType;
|
||||
|
||||
final newSentences = result.list
|
||||
.map(HomeSentence.fromFeedItem)
|
||||
.where((s) => s.text.isNotEmpty)
|
||||
.where(
|
||||
(s) =>
|
||||
enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType),
|
||||
(s) {
|
||||
// 选择了具体分类时,严格过滤只保留该分类
|
||||
if (selectedType != null) {
|
||||
return s.feedType == selectedType;
|
||||
}
|
||||
// "推荐"模式下,只显示启用的分类
|
||||
return enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType);
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -118,6 +127,7 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
hasMore: true,
|
||||
isOffline: false,
|
||||
isCycling: false,
|
||||
isCategorySwitching: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
);
|
||||
@@ -143,6 +153,7 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
hasMore: true,
|
||||
isOffline: false,
|
||||
isCycling: false,
|
||||
isCategorySwitching: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
);
|
||||
@@ -561,14 +572,22 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
}
|
||||
|
||||
final enabledChannelKeys = state.channels.map((c) => c.key).toSet();
|
||||
// 当选择了具体分类时,只保留该分类的数据
|
||||
final selectedType = state.selectedType;
|
||||
|
||||
final newSentences = result.list
|
||||
.map(HomeSentence.fromFeedItem)
|
||||
.where((s) => s.text.isNotEmpty)
|
||||
.where(
|
||||
(s) =>
|
||||
enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType),
|
||||
(s) {
|
||||
// 选择了具体分类时,严格过滤只保留该分类
|
||||
if (selectedType != null) {
|
||||
return s.feedType == selectedType;
|
||||
}
|
||||
// "推荐"模式下,只显示启用的分类
|
||||
return enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType);
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -622,9 +641,13 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
.map(HomeSentence.fromFeedItem)
|
||||
.where((s) => s.text.isNotEmpty)
|
||||
.where(
|
||||
(s) =>
|
||||
enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType),
|
||||
(s) {
|
||||
if (selectedType != null) {
|
||||
return s.feedType == selectedType;
|
||||
}
|
||||
return enabledChannelKeys.isEmpty ||
|
||||
enabledChannelKeys.contains(s.feedType);
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
if (fallback.isNotEmpty) {
|
||||
@@ -653,6 +676,7 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
hasMore: true,
|
||||
isOffline: false,
|
||||
isCycling: false,
|
||||
isCategorySwitching: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
);
|
||||
@@ -713,6 +737,7 @@ mixin HomeFeedMixin on Notifier<HomeState> {
|
||||
hasMore: true,
|
||||
isOffline: false,
|
||||
isCycling: false,
|
||||
isCategorySwitching: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
);
|
||||
|
||||
@@ -269,6 +269,37 @@ class HomeNotifier extends Notifier<HomeState>
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅刷新句子广场列表,不刷新每日句子卡片等其他内容
|
||||
Future<void> refreshSentenceList() async {
|
||||
final key = cacheKey;
|
||||
_categoryCache.remove(key);
|
||||
for (final s in state.sentences) {
|
||||
if (_allSeenIds.length >= _maxSeenSize) _allSeenIds.clear();
|
||||
_allSeenIds.add(s.id);
|
||||
if (_deduplicateContent && s.text.isNotEmpty) {
|
||||
if (_allSeenTexts.length >= _maxSeenSize) _allSeenTexts.clear();
|
||||
_allSeenTexts.add(s.text.trim());
|
||||
}
|
||||
}
|
||||
state = state.copyWith(
|
||||
isLoading: true,
|
||||
isForceLoading: true,
|
||||
sentences: [],
|
||||
isCycling: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
isCategorySwitching: false,
|
||||
);
|
||||
_currentPage = 1;
|
||||
_lastFeedId = null;
|
||||
_isLoadingMore = false;
|
||||
try {
|
||||
await fetchRefreshSentences();
|
||||
} catch (e) {
|
||||
Log.e('句子列表刷新失败', e, null, LogCategory.provider);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> smartRefresh() async {
|
||||
if (state.sentences.isEmpty) {
|
||||
await refresh();
|
||||
@@ -323,26 +354,36 @@ class HomeNotifier extends Notifier<HomeState>
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
selectedType: type,
|
||||
clearType: type == null,
|
||||
isCycling: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
isLoading: true,
|
||||
);
|
||||
|
||||
if (cached != null && cached.isNotEmpty) {
|
||||
// 有缓存:先显示缓存数据,后台静默刷新
|
||||
state = state.copyWith(
|
||||
selectedType: type,
|
||||
clearType: type == null,
|
||||
sentences: cached,
|
||||
isLoading: false,
|
||||
isForceLoading: false,
|
||||
hasMore: true,
|
||||
isCycling: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
isCategorySwitching: false,
|
||||
);
|
||||
_currentPage = (cached.length ~/ 20) + 1;
|
||||
_silentRefresh();
|
||||
} else {
|
||||
state = state.copyWith(isForceLoading: true);
|
||||
// 无缓存:清空旧数据,显示骨架屏加载状态,等待新数据到达
|
||||
state = state.copyWith(
|
||||
selectedType: type,
|
||||
clearType: type == null,
|
||||
sentences: [],
|
||||
isLoading: true,
|
||||
isForceLoading: true,
|
||||
hasMore: true,
|
||||
isCycling: false,
|
||||
cycleRound: 0,
|
||||
lastCycleIds: [],
|
||||
isCategorySwitching: true,
|
||||
);
|
||||
await fetchNewSentences(replace: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 首页状态
|
||||
/// 创建时间: 2026-05-12
|
||||
/// 更新时间: 2026-05-12
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 首页状态模型定义
|
||||
/// 上次更新: 从home_provider.dart拆分
|
||||
/// 上次更新: 新增isCategorySwitching标志,用于分类切换后的空状态tips
|
||||
/// ============================================================
|
||||
|
||||
import '../models/feed_model.dart';
|
||||
@@ -24,6 +24,7 @@ class HomeState {
|
||||
this.cycleRound = 0,
|
||||
this.lastCycleIds = const <String>[],
|
||||
this.isForceLoading = false,
|
||||
this.isCategorySwitching = false,
|
||||
});
|
||||
|
||||
final HomeSentence? dailySentence;
|
||||
@@ -46,6 +47,9 @@ class HomeState {
|
||||
|
||||
final bool isForceLoading;
|
||||
|
||||
/// 分类切换中标志:切换分类后数据为空时显示倒计时tips,数据到达后自动清除
|
||||
final bool isCategorySwitching;
|
||||
|
||||
HomeState copyWith({
|
||||
HomeSentence? dailySentence,
|
||||
bool clearDaily = false,
|
||||
@@ -62,6 +66,7 @@ class HomeState {
|
||||
int? cycleRound,
|
||||
List<String>? lastCycleIds,
|
||||
bool? isForceLoading,
|
||||
bool? isCategorySwitching,
|
||||
}) {
|
||||
return HomeState(
|
||||
dailySentence: clearDaily ? null : (dailySentence ?? this.dailySentence),
|
||||
@@ -77,6 +82,7 @@ class HomeState {
|
||||
cycleRound: cycleRound ?? this.cycleRound,
|
||||
lastCycleIds: lastCycleIds ?? this.lastCycleIds,
|
||||
isForceLoading: isForceLoading ?? this.isForceLoading,
|
||||
isCategorySwitching: isCategorySwitching ?? this.isCategorySwitching,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// ============================================================
|
||||
// 闲言APP — 句子来源状态管理
|
||||
// 创建时间: 2026-04-29
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 频道数据+开关状态+统计信息+混合规则+偏好设置管理
|
||||
/// 上次更新: 频道重排序(节气/文章/论语/缩写/酒方置底默认关闭)
|
||||
/// 上次更新: 移除硬编码禁用列表,分类启用状态由服务端推荐权重管理控制
|
||||
// ============================================================
|
||||
|
||||
import 'dart:convert';
|
||||
@@ -22,7 +22,7 @@ const _kDeduplicateContent = 'source_deduplicate_content';
|
||||
const _kSortPreference = 'source_sort_preference';
|
||||
const _kPerPagePreference = 'source_per_page_preference';
|
||||
|
||||
const _kDefaultDisabledKeys = {'jieqi', 'article', 'lunyu', 'abbr', 'jiufang'};
|
||||
/// 后台默认关闭的分类key(服务端已根据is_enabled过滤,此处仅用于重排序置底)
|
||||
const _kBottomKeys = ['jieqi', 'article', 'lunyu', 'abbr', 'jiufang'];
|
||||
|
||||
class SourceState {
|
||||
@@ -123,14 +123,12 @@ class SourceNotifier extends Notifier<SourceState> {
|
||||
|
||||
final reordered = _reorderChannels(rawChannels);
|
||||
|
||||
// 服务端已根据is_enabled过滤,前端无需默认禁用
|
||||
var disabledKeys = state.disabledKeys;
|
||||
if (disabledKeys.isEmpty) {
|
||||
final defaultDisabled = rawChannels
|
||||
.where((c) => _kDefaultDisabledKeys.contains(c.key))
|
||||
.map((c) => c.key)
|
||||
.toSet();
|
||||
disabledKeys = defaultDisabled;
|
||||
await _saveDisabledKeys(defaultDisabled);
|
||||
// 首次使用时,不设置默认禁用,所有服务端返回的分类默认启用
|
||||
disabledKeys = {};
|
||||
await _saveDisabledKeys({});
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
|
||||
@@ -63,7 +63,7 @@ const ar = T(
|
||||
createCard: 'إنشاء بطاقة',
|
||||
editThisSentence: 'تعديل الاقتباس',
|
||||
noSentences: 'لا توجد اقتباسات',
|
||||
pullDownToRefresh: 'اسحب للتحديث',
|
||||
pullDownToRefresh: 'حاول التحديث',
|
||||
networkConnectionFailed: 'فشل اتصال الشبكة',
|
||||
clickToRetry: 'اضغط لإعادة المحاولة',
|
||||
sentenceCopied: 'تم نسخ الاقتباس',
|
||||
@@ -81,6 +81,8 @@ const ar = T(
|
||||
longPressToSet: 'اضغط مطولاً للإعداد',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'فشل المشاركة',
|
||||
loadingContent: 'جاري تحميل المحتوى...',
|
||||
autoRefreshSeconds: 'تحديث تلقائي بعد {0} ثانية',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'اضغط مطولاً للتحديد',
|
||||
|
||||
@@ -63,7 +63,7 @@ const bn = T(
|
||||
createCard: 'কার্ড তৈরি',
|
||||
editThisSentence: 'উক্তি সম্পাদনা',
|
||||
noSentences: 'কোনো উক্তি নেই',
|
||||
pullDownToRefresh: 'রিফ্রেশ করতে টানুন',
|
||||
pullDownToRefresh: 'রিফ্রেশ করুন',
|
||||
networkConnectionFailed: 'নেটওয়ার্ক সংযোগ ব্যর্থ',
|
||||
clickToRetry: 'আবার চেষ্টা করতে ট্যাপ করুন',
|
||||
sentenceCopied: 'উক্তি কপি হয়েছে',
|
||||
@@ -81,6 +81,8 @@ const bn = T(
|
||||
longPressToSet: 'সেট করতে দীর্ঘ চাপুন',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'শেয়ার ব্যর্থ',
|
||||
loadingContent: 'কন্টেন্ট লোড হচ্ছে...',
|
||||
autoRefreshSeconds: '{0} সেকেন্ডে স্বয়ংক্রিয় রিফ্রেশ',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'নির্বাচন করতে দীর্ঘক্ষণ চাপুন',
|
||||
|
||||
@@ -63,7 +63,7 @@ const de = T(
|
||||
createCard: 'Karte erstellen',
|
||||
editThisSentence: 'Zitat bearbeiten',
|
||||
noSentences: 'Keine Zitate',
|
||||
pullDownToRefresh: 'Ziehen zum Aktualisieren',
|
||||
pullDownToRefresh: 'Versuchen Sie zu aktualisieren',
|
||||
networkConnectionFailed: 'Netzwerkverbindung fehlgeschlagen',
|
||||
clickToRetry: 'Tippen zum Wiederholen',
|
||||
sentenceCopied: 'Zitat kopiert',
|
||||
@@ -81,6 +81,8 @@ const de = T(
|
||||
longPressToSet: 'Gedrückt halten zum Einstellen',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Teilen fehlgeschlagen',
|
||||
loadingContent: 'Inhalt wird geladen...',
|
||||
autoRefreshSeconds: 'Automatische Aktualisierung in {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Lange drücken zum Auswählen',
|
||||
|
||||
@@ -63,7 +63,7 @@ const en = T(
|
||||
createCard: 'Create Card',
|
||||
editThisSentence: 'Edit Quote',
|
||||
noSentences: 'No quotes yet',
|
||||
pullDownToRefresh: 'Pull down to refresh',
|
||||
pullDownToRefresh: 'Try refreshing',
|
||||
networkConnectionFailed: 'Network connection failed',
|
||||
clickToRetry: 'Tap to retry',
|
||||
sentenceCopied: 'Quote copied',
|
||||
@@ -81,6 +81,8 @@ const en = T(
|
||||
longPressToSet: 'Long press to set',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Share failed',
|
||||
loadingContent: 'Loading content...',
|
||||
autoRefreshSeconds: 'Auto-refresh in {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Long press to select text',
|
||||
|
||||
@@ -63,7 +63,7 @@ const es = T(
|
||||
createCard: 'Crear tarjeta',
|
||||
editThisSentence: 'Editar cita',
|
||||
noSentences: 'Sin citas',
|
||||
pullDownToRefresh: 'Desliza para actualizar',
|
||||
pullDownToRefresh: 'Intenta actualizar',
|
||||
networkConnectionFailed: 'Conexión de red fallida',
|
||||
clickToRetry: 'Toca para reintentar',
|
||||
sentenceCopied: 'Cita copiada',
|
||||
@@ -81,6 +81,8 @@ const es = T(
|
||||
longPressToSet: 'Mantener para configurar',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Error al compartir',
|
||||
loadingContent: 'Cargando contenido...',
|
||||
autoRefreshSeconds: 'Actualización automática en {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Mantén presionado para seleccionar',
|
||||
|
||||
@@ -62,7 +62,7 @@ const fr = T(
|
||||
createCard: 'Créer une carte',
|
||||
editThisSentence: 'Modifier la citation',
|
||||
noSentences: 'Aucune citation',
|
||||
pullDownToRefresh: 'Tirer pour actualiser',
|
||||
pullDownToRefresh: 'Essayez d\'actualiser',
|
||||
networkConnectionFailed: 'Échec de connexion réseau',
|
||||
clickToRetry: 'Appuyez pour réessayer',
|
||||
sentenceCopied: 'Citation copiée',
|
||||
@@ -80,6 +80,8 @@ const fr = T(
|
||||
longPressToSet: 'Appui long pour configurer',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Échec du partage',
|
||||
loadingContent: 'Chargement du contenu...',
|
||||
autoRefreshSeconds: 'Actualisation automatique dans {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Appuyez longuement pour sélectionner',
|
||||
|
||||
@@ -62,7 +62,7 @@ const hi = T(
|
||||
createCard: 'कार्ड बनाएं',
|
||||
editThisSentence: 'उद्धरण संपादित करें',
|
||||
noSentences: 'कोई उद्धरण नहीं',
|
||||
pullDownToRefresh: 'रिफ्रेश करने के लिए खींचें',
|
||||
pullDownToRefresh: 'रिफ्रेश करें',
|
||||
networkConnectionFailed: 'नेटवर्क कनेक्शन विफल',
|
||||
clickToRetry: 'पुनः प्रयास करने के लिए टैप करें',
|
||||
sentenceCopied: 'उद्धरण कॉपी किया गया',
|
||||
@@ -80,6 +80,8 @@ const hi = T(
|
||||
longPressToSet: 'सेट करने के लिए देर तक दबाएं',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'साझा करना विफल',
|
||||
loadingContent: 'सामग्री लोड हो रही है...',
|
||||
autoRefreshSeconds: '{0} सेकंड में स्वतः रिफ्रेश',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'चुनने के लिए देर तक दबाएं',
|
||||
|
||||
@@ -62,7 +62,7 @@ const it = T(
|
||||
createCard: 'Crea scheda',
|
||||
editThisSentence: 'Modifica citazione',
|
||||
noSentences: 'Nessuna citazione',
|
||||
pullDownToRefresh: 'Trascina per aggiornare',
|
||||
pullDownToRefresh: 'Prova ad aggiornare',
|
||||
networkConnectionFailed: 'Connessione di rete fallita',
|
||||
clickToRetry: 'Tocca per riprovare',
|
||||
sentenceCopied: 'Citazione copiata',
|
||||
@@ -80,6 +80,8 @@ const it = T(
|
||||
longPressToSet: 'Tieni premuto per impostare',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Condivisione fallita',
|
||||
loadingContent: 'Caricamento contenuti...',
|
||||
autoRefreshSeconds: 'Aggiornamento automatico tra {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Tieni premuto per selezionare',
|
||||
|
||||
@@ -62,7 +62,7 @@ const ja = T(
|
||||
createCard: 'カードを作成',
|
||||
editThisSentence: 'この文を編集',
|
||||
noSentences: '言葉がありません',
|
||||
pullDownToRefresh: '下に引いて更新',
|
||||
pullDownToRefresh: '更新してみてください',
|
||||
networkConnectionFailed: 'ネットワーク接続に失敗',
|
||||
clickToRetry: 'タップして再試行',
|
||||
sentenceCopied: '言葉をコピーしました',
|
||||
@@ -80,6 +80,8 @@ const ja = T(
|
||||
longPressToSet: '長押しで設定',
|
||||
shareAppSignature: '— 閑言APP',
|
||||
shareFailed: '共有に失敗',
|
||||
loadingContent: 'コンテンツを読み込み中...',
|
||||
autoRefreshSeconds: '{0}秒後に自動更新',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: '長押しでテキストを選択',
|
||||
|
||||
@@ -62,7 +62,7 @@ const ko = T(
|
||||
createCard: '카드 만들기',
|
||||
editThisSentence: '문장 편집',
|
||||
noSentences: '문장이 없습니다',
|
||||
pullDownToRefresh: '당겨서 새로고침',
|
||||
pullDownToRefresh: '새로고침 해보세요',
|
||||
networkConnectionFailed: '네트워크 연결 실패',
|
||||
clickToRetry: '탭하여 재시도',
|
||||
sentenceCopied: '문장이 복사되었습니다',
|
||||
@@ -80,6 +80,8 @@ const ko = T(
|
||||
longPressToSet: '길게 눌러 설정',
|
||||
shareAppSignature: '— 셴옌APP',
|
||||
shareFailed: '공유 실패',
|
||||
loadingContent: '콘텐츠 로딩 중...',
|
||||
autoRefreshSeconds: '{0}초 후 자동 새로고침',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: '길게 눌러 텍스트 선택',
|
||||
|
||||
@@ -62,7 +62,7 @@ const pt = T(
|
||||
createCard: 'Criar cartão',
|
||||
editThisSentence: 'Editar citação',
|
||||
noSentences: 'Sem citações',
|
||||
pullDownToRefresh: 'Puxe para atualizar',
|
||||
pullDownToRefresh: 'Tente atualizar',
|
||||
networkConnectionFailed: 'Falha na conexão de rede',
|
||||
clickToRetry: 'Toque para tentar novamente',
|
||||
sentenceCopied: 'Citação copiada',
|
||||
@@ -80,6 +80,8 @@ const pt = T(
|
||||
longPressToSet: 'Pressione para configurar',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Falha ao compartilhar',
|
||||
loadingContent: 'Carregando conteúdo...',
|
||||
autoRefreshSeconds: 'Atualização automática em {0}s',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Toque longo para selecionar',
|
||||
|
||||
@@ -62,7 +62,7 @@ const ru = T(
|
||||
createCard: 'Создать карточку',
|
||||
editThisSentence: 'Редактировать цитату',
|
||||
noSentences: 'Нет цитат',
|
||||
pullDownToRefresh: 'Потяните для обновления',
|
||||
pullDownToRefresh: 'Попробуйте обновить',
|
||||
networkConnectionFailed: 'Сбой подключения к сети',
|
||||
clickToRetry: 'Нажмите для повтора',
|
||||
sentenceCopied: 'Цитата скопирована',
|
||||
@@ -80,6 +80,8 @@ const ru = T(
|
||||
longPressToSet: 'Удерживайте для настройки',
|
||||
shareAppSignature: '— Xianyan APP',
|
||||
shareFailed: 'Не удалось поделиться',
|
||||
loadingContent: 'Загрузка контента...',
|
||||
autoRefreshSeconds: 'Автообновление через {0}с',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: 'Долгое нажатие для выделения',
|
||||
|
||||
@@ -62,7 +62,7 @@ const zhCN = T(
|
||||
createCard: '创作卡片',
|
||||
editThisSentence: '编辑此句',
|
||||
noSentences: '暂无句子',
|
||||
pullDownToRefresh: '下拉刷新试试',
|
||||
pullDownToRefresh: '刷新试试',
|
||||
networkConnectionFailed: '网络连接失败',
|
||||
clickToRetry: '点击重试',
|
||||
sentenceCopied: '已复制句子',
|
||||
@@ -80,6 +80,8 @@ const zhCN = T(
|
||||
longPressToSet: '长按设置',
|
||||
shareAppSignature: '— 闲言APP',
|
||||
shareFailed: '分享失败',
|
||||
loadingContent: '正在加载内容...',
|
||||
autoRefreshSeconds: '{0}秒后自动刷新',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: '长按可选择文字',
|
||||
|
||||
@@ -62,7 +62,7 @@ const zhTW = T(
|
||||
createCard: '創作卡片',
|
||||
editThisSentence: '編輯此句',
|
||||
noSentences: '暫無句子',
|
||||
pullDownToRefresh: '下拉重新整理試試',
|
||||
pullDownToRefresh: '重新整理試試',
|
||||
networkConnectionFailed: '網路連線失敗',
|
||||
clickToRetry: '點擊重試',
|
||||
sentenceCopied: '已複製句子',
|
||||
@@ -80,6 +80,8 @@ const zhTW = T(
|
||||
longPressToSet: '長按設定',
|
||||
shareAppSignature: '— 閑言APP',
|
||||
shareFailed: '分享失敗',
|
||||
loadingContent: '正在載入內容...',
|
||||
autoRefreshSeconds: '{0}秒後自動刷新',
|
||||
),
|
||||
sentenceDetail: TSentenceDetail(
|
||||
longPressToSelect: '長按可選擇文字',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 首页基础翻译类型
|
||||
/// 创建时间: 2026-05-31
|
||||
/// 更新时间: 2026-05-31
|
||||
/// 更新时间: 2026-06-07
|
||||
/// 作用: 首页基础翻译键定义(电池提示、默认句子、广场Header、工具中心等)
|
||||
/// 上次更新: fromMap新增fallback参数支持导入时回退
|
||||
/// 上次更新: 新增loadingContent/autoRefreshCountdown多语言字段
|
||||
/// ============================================================
|
||||
|
||||
class THomeBase {
|
||||
@@ -39,6 +39,8 @@ class THomeBase {
|
||||
required this.longPressToSet,
|
||||
required this.shareAppSignature,
|
||||
required this.shareFailed,
|
||||
required this.loadingContent,
|
||||
required this.autoRefreshSeconds,
|
||||
});
|
||||
|
||||
/// 电量很低了!快充电 🔴
|
||||
@@ -67,7 +69,7 @@ class THomeBase {
|
||||
final String editThisSentence;
|
||||
/// 暂无句子
|
||||
final String noSentences;
|
||||
/// 下拉刷新试试
|
||||
/// 刷新试试
|
||||
final String pullDownToRefresh;
|
||||
/// 网络连接失败
|
||||
final String networkConnectionFailed;
|
||||
@@ -103,6 +105,14 @@ class THomeBase {
|
||||
final String shareAppSignature;
|
||||
/// 分享失败
|
||||
final String shareFailed;
|
||||
/// 正在加载内容...
|
||||
final String loadingContent;
|
||||
/// {0}秒后自动刷新
|
||||
final String autoRefreshSeconds;
|
||||
|
||||
/// 倒计时自动刷新文案,如"3秒后自动刷新"
|
||||
String autoRefreshCountdown(int seconds) =>
|
||||
autoRefreshSeconds.replaceAll('{0}', '$seconds');
|
||||
|
||||
Map<String, String> toMap() => {
|
||||
'batteryCritical': batteryCritical,
|
||||
@@ -136,6 +146,8 @@ class THomeBase {
|
||||
'longPressToSet': longPressToSet,
|
||||
'shareAppSignature': shareAppSignature,
|
||||
'shareFailed': shareFailed,
|
||||
'loadingContent': loadingContent,
|
||||
'autoRefreshSeconds': autoRefreshSeconds,
|
||||
};
|
||||
|
||||
static THomeBase fromMap(Map<String, String> map, {THomeBase? fallback}) =>
|
||||
@@ -234,5 +246,11 @@ class THomeBase {
|
||||
shareFailed: map['shareFailed']?.isNotEmpty == true
|
||||
? map['shareFailed']!
|
||||
: (fallback?.shareFailed ?? ''),
|
||||
loadingContent: map['loadingContent']?.isNotEmpty == true
|
||||
? map['loadingContent']!
|
||||
: (fallback?.loadingContent ?? ''),
|
||||
autoRefreshSeconds: map['autoRefreshSeconds']?.isNotEmpty == true
|
||||
? map['autoRefreshSeconds']!
|
||||
: (fallback?.autoRefreshSeconds ?? ''),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import flutter_app_group_directory
|
||||
import flutter_image_compress_macos
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_secure_storage_darwin
|
||||
import flutter_tts
|
||||
import flutter_webrtc
|
||||
import gal
|
||||
@@ -55,7 +55,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
|
||||
FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
name: xianyan
|
||||
description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP"
|
||||
publish_to: 'none'
|
||||
version: 6.6.6+2606061
|
||||
version: 6.6.7+2606071
|
||||
# 年月日-次 7位
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user