MacBook端提交

This commit is contained in:
Developer
2026-06-07 18:20:26 +08:00
parent ff08e6c128
commit e933f2160d
29 changed files with 430 additions and 75 deletions

View File

@@ -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

View File

@@ -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'),

View File

@@ -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([

View File

@@ -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: '作文大全',

View File

@@ -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 =

View File

@@ -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,
};
}
}

View File

@@ -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,
),

View File

@@ -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,
),
),
),
],
),

View File

@@ -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: [],
);

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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(

View File

@@ -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: 'اضغط مطولاً للتحديد',

View File

@@ -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: 'নির্বাচন করতে দীর্ঘক্ষণ চাপুন',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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: 'चुनने के लिए देर तक दबाएं',

View File

@@ -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',

View File

@@ -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: '長押しでテキストを選択',

View File

@@ -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: '길게 눌러 텍스트 선택',

View File

@@ -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',

View File

@@ -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: 'Долгое нажатие для выделения',

View File

@@ -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: '长按可选择文字',

View File

@@ -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: '長按可選擇文字',

View File

@@ -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 ?? ''),
);
}

View File

@@ -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"))

View File

@@ -21,7 +21,7 @@
name: xianyan
description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP"
publish_to: 'none'
version: 6.6.6+2606061
version: 6.6.7+2606071
# 年月日-次 7位
environment: