From e933f2160d5627a33d0524f4a1ecb5d3ef2f54e7 Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 7 Jun 2026 18:20:26 +0800 Subject: [PATCH] =?UTF-8?q?MacBook=E7=AB=AF=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 44 +++++ .../application/api/controller/Feed.php | 35 +++- .../presentation/daily_card_ar_view.dart | 31 ++-- lib/features/discover/models/tool_item.dart | 6 + .../providers/tool_center_provider.dart | 7 +- lib/features/home/models/feed_model.dart | 13 +- lib/features/home/presentation/home_page.dart | 2 + .../widgets/home_sentence_list_section.dart | 151 +++++++++++++++++- .../home/providers/home_feed_mixin.dart | 43 +++-- .../home/providers/home_provider.dart | 61 +++++-- lib/features/home/providers/home_state.dart | 10 +- .../source/providers/source_provider.dart | 16 +- lib/l10n/languages/ar.dart | 4 +- lib/l10n/languages/bn.dart | 4 +- lib/l10n/languages/de.dart | 4 +- lib/l10n/languages/en.dart | 4 +- lib/l10n/languages/es.dart | 4 +- lib/l10n/languages/fr.dart | 4 +- lib/l10n/languages/hi.dart | 4 +- lib/l10n/languages/it.dart | 4 +- lib/l10n/languages/ja.dart | 4 +- lib/l10n/languages/ko.dart | 4 +- lib/l10n/languages/pt.dart | 4 +- lib/l10n/languages/ru.dart | 4 +- lib/l10n/languages/zh_cn.dart | 4 +- lib/l10n/languages/zh_tw.dart | 4 +- lib/l10n/types/t_home_base.dart | 24 ++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- pubspec.macos.yaml | 2 +- 29 files changed, 430 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daed0822..e3d3630c 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/docs/toolsapi/application/api/controller/Feed.php b/docs/toolsapi/application/api/controller/Feed.php index dc7e9ca6..6bc3d9c1 100644 --- a/docs/toolsapi/application/api/controller/Feed.php +++ b/docs/toolsapi/application/api/controller/Feed.php @@ -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'), diff --git a/lib/features/daily_card/presentation/daily_card_ar_view.dart b/lib/features/daily_card/presentation/daily_card_ar_view.dart index 528d3885..46a70329 100644 --- a/lib/features/daily_card/presentation/daily_card_ar_view.dart +++ b/lib/features/daily_card/presentation/daily_card_ar_view.dart @@ -202,34 +202,32 @@ class _DailyCardArViewState extends ConsumerState // ---- 手势处理 ---- - 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 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([ diff --git a/lib/features/discover/models/tool_item.dart b/lib/features/discover/models/tool_item.dart index 29f3db13..d14795a8 100644 --- a/lib/features/discover/models/tool_item.dart +++ b/lib/features/discover/models/tool_item.dart @@ -948,6 +948,7 @@ const defaultTools = [ presetKeywords: ['天气', '农事', '健康', '学习', '做人'], ), ), + // TODO: 歌词大全 - 暂时隐藏,待内容完善后恢复 ToolItem( id: 'lyric', name: '歌词大全', @@ -957,6 +958,7 @@ const defaultTools = [ route: '/tool/lyric', description: '歌词搜索', apiPath: '/api/hanzi/search', + isDeleted: true, navConfig: ToolNavConfig.list( toolId: 'lyric', title: '歌词大全', @@ -967,6 +969,7 @@ const defaultTools = [ presetKeywords: ['月亮', '春天', '爱情', '故乡', '朋友'], ), ), + // TODO: 故事大全 - 暂时隐藏,待内容完善后恢复 ToolItem( id: 'story', name: '故事大全', @@ -976,6 +979,7 @@ const defaultTools = [ route: '/tool/story', description: '故事搜索阅读', apiPath: '/api/hanzi/search', + isDeleted: true, navConfig: ToolNavConfig.list( toolId: 'story', title: '故事大全', @@ -986,6 +990,7 @@ const defaultTools = [ presetKeywords: ['公主', '王子', '龙', '魔法', '冒险'], ), ), + // TODO: 作文大全 - 暂时隐藏,待内容完善后恢复 ToolItem( id: 'zuowen', name: '作文大全', @@ -995,6 +1000,7 @@ const defaultTools = [ route: '/tool/zuowen', description: '优秀作文参考', apiPath: '/api/hanzi/search', + isDeleted: true, navConfig: ToolNavConfig.list( toolId: 'zuowen', title: '作文大全', diff --git a/lib/features/discover/providers/tool_center_provider.dart b/lib/features/discover/providers/tool_center_provider.dart index 70185ae2..f3247863 100644 --- a/lib/features/discover/providers/tool_center_provider.dart +++ b/lib/features/discover/providers/tool_center_provider.dart @@ -419,8 +419,11 @@ class ToolCenterNotifier extends Notifier { 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 = diff --git a/lib/features/home/models/feed_model.dart b/lib/features/home/models/feed_model.dart index fb12d389..67e0d5d6 100644 --- a/lib/features/home/models/feed_model.dart +++ b/lib/features/home/models/feed_model.dart @@ -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 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 toJson() { - return {'key': key, 'name': name, 'icon': icon, 'count': count}; + return { + 'key': key, + 'name': name, + 'icon': icon, + 'count': count, + 'is_enabled': isEnabled, + }; } } diff --git a/lib/features/home/presentation/home_page.dart b/lib/features/home/presentation/home_page.dart index c465ecb5..9b16dfb7 100644 --- a/lib/features/home/presentation/home_page.dart +++ b/lib/features/home/presentation/home_page.dart @@ -468,6 +468,8 @@ class _HomePageState extends ConsumerState { 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, ), diff --git a/lib/features/home/presentation/widgets/home_sentence_list_section.dart b/lib/features/home/presentation/widgets/home_sentence_list_section.dart index 1181d7b8..3690dee4 100644 --- a/lib/features/home/presentation/widgets/home_sentence_list_section.dart +++ b/lib/features/home/presentation/widgets/home_sentence_list_section.dart @@ -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 { +class _HomeSentenceListSectionState extends ConsumerState + 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 + 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 0.5 + ? CupertinoColors.black + : CupertinoColors.white, + ), + ), ), ], ), diff --git a/lib/features/home/providers/home_feed_mixin.dart b/lib/features/home/providers/home_feed_mixin.dart index 53a93562..edfc12fb 100644 --- a/lib/features/home/providers/home_feed_mixin.dart +++ b/lib/features/home/providers/home_feed_mixin.dart @@ -84,13 +84,22 @@ mixin HomeFeedMixin on Notifier { } 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 { hasMore: true, isOffline: false, isCycling: false, + isCategorySwitching: false, cycleRound: 0, lastCycleIds: [], ); @@ -143,6 +153,7 @@ mixin HomeFeedMixin on Notifier { hasMore: true, isOffline: false, isCycling: false, + isCategorySwitching: false, cycleRound: 0, lastCycleIds: [], ); @@ -561,14 +572,22 @@ mixin HomeFeedMixin on Notifier { } 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 { .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 { hasMore: true, isOffline: false, isCycling: false, + isCategorySwitching: false, cycleRound: 0, lastCycleIds: [], ); @@ -713,6 +737,7 @@ mixin HomeFeedMixin on Notifier { hasMore: true, isOffline: false, isCycling: false, + isCategorySwitching: false, cycleRound: 0, lastCycleIds: [], ); diff --git a/lib/features/home/providers/home_provider.dart b/lib/features/home/providers/home_provider.dart index de74adf5..8f045ac8 100644 --- a/lib/features/home/providers/home_provider.dart +++ b/lib/features/home/providers/home_provider.dart @@ -269,6 +269,37 @@ class HomeNotifier extends Notifier } } + /// 仅刷新句子广场列表,不刷新每日句子卡片等其他内容 + Future 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 smartRefresh() async { if (state.sentences.isEmpty) { await refresh(); @@ -323,26 +354,36 @@ class HomeNotifier extends Notifier } } - 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); } } diff --git a/lib/features/home/providers/home_state.dart b/lib/features/home/providers/home_state.dart index 73741935..e4209717 100644 --- a/lib/features/home/providers/home_state.dart +++ b/lib/features/home/providers/home_state.dart @@ -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 [], 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? 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, ); } } diff --git a/lib/features/source/providers/source_provider.dart b/lib/features/source/providers/source_provider.dart index abf3071f..7d50681f 100644 --- a/lib/features/source/providers/source_provider.dart +++ b/lib/features/source/providers/source_provider.dart @@ -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 { 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( diff --git a/lib/l10n/languages/ar.dart b/lib/l10n/languages/ar.dart index c1e7e7af..41442b0a 100644 --- a/lib/l10n/languages/ar.dart +++ b/lib/l10n/languages/ar.dart @@ -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: 'اضغط مطولاً للتحديد', diff --git a/lib/l10n/languages/bn.dart b/lib/l10n/languages/bn.dart index 2f785e60..c23a83aa 100644 --- a/lib/l10n/languages/bn.dart +++ b/lib/l10n/languages/bn.dart @@ -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: 'নির্বাচন করতে দীর্ঘক্ষণ চাপুন', diff --git a/lib/l10n/languages/de.dart b/lib/l10n/languages/de.dart index c07b6fc0..dea5d76d 100644 --- a/lib/l10n/languages/de.dart +++ b/lib/l10n/languages/de.dart @@ -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', diff --git a/lib/l10n/languages/en.dart b/lib/l10n/languages/en.dart index 72ac48e3..951024f0 100644 --- a/lib/l10n/languages/en.dart +++ b/lib/l10n/languages/en.dart @@ -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', diff --git a/lib/l10n/languages/es.dart b/lib/l10n/languages/es.dart index af09f4ce..86a68b05 100644 --- a/lib/l10n/languages/es.dart +++ b/lib/l10n/languages/es.dart @@ -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', diff --git a/lib/l10n/languages/fr.dart b/lib/l10n/languages/fr.dart index 6053f29c..d9a84df2 100644 --- a/lib/l10n/languages/fr.dart +++ b/lib/l10n/languages/fr.dart @@ -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', diff --git a/lib/l10n/languages/hi.dart b/lib/l10n/languages/hi.dart index ac5beaf2..4b4f6c75 100644 --- a/lib/l10n/languages/hi.dart +++ b/lib/l10n/languages/hi.dart @@ -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: 'चुनने के लिए देर तक दबाएं', diff --git a/lib/l10n/languages/it.dart b/lib/l10n/languages/it.dart index ba4eae1e..86b5f271 100644 --- a/lib/l10n/languages/it.dart +++ b/lib/l10n/languages/it.dart @@ -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', diff --git a/lib/l10n/languages/ja.dart b/lib/l10n/languages/ja.dart index fe99fc37..19471912 100644 --- a/lib/l10n/languages/ja.dart +++ b/lib/l10n/languages/ja.dart @@ -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: '長押しでテキストを選択', diff --git a/lib/l10n/languages/ko.dart b/lib/l10n/languages/ko.dart index 6b9a9b22..34f8ca88 100644 --- a/lib/l10n/languages/ko.dart +++ b/lib/l10n/languages/ko.dart @@ -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: '길게 눌러 텍스트 선택', diff --git a/lib/l10n/languages/pt.dart b/lib/l10n/languages/pt.dart index b33cd2d8..87e9580f 100644 --- a/lib/l10n/languages/pt.dart +++ b/lib/l10n/languages/pt.dart @@ -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', diff --git a/lib/l10n/languages/ru.dart b/lib/l10n/languages/ru.dart index 5b7873af..bb7bfe69 100644 --- a/lib/l10n/languages/ru.dart +++ b/lib/l10n/languages/ru.dart @@ -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: 'Долгое нажатие для выделения', diff --git a/lib/l10n/languages/zh_cn.dart b/lib/l10n/languages/zh_cn.dart index 7a6d4991..a9f422f0 100644 --- a/lib/l10n/languages/zh_cn.dart +++ b/lib/l10n/languages/zh_cn.dart @@ -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: '长按可选择文字', diff --git a/lib/l10n/languages/zh_tw.dart b/lib/l10n/languages/zh_tw.dart index 7891ad56..56839112 100644 --- a/lib/l10n/languages/zh_tw.dart +++ b/lib/l10n/languages/zh_tw.dart @@ -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: '長按可選擇文字', diff --git a/lib/l10n/types/t_home_base.dart b/lib/l10n/types/t_home_base.dart index 01a5f6d5..a58e917d 100644 --- a/lib/l10n/types/t_home_base.dart +++ b/lib/l10n/types/t_home_base.dart @@ -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 toMap() => { 'batteryCritical': batteryCritical, @@ -136,6 +146,8 @@ class THomeBase { 'longPressToSet': longPressToSet, 'shareAppSignature': shareAppSignature, 'shareFailed': shareFailed, + 'loadingContent': loadingContent, + 'autoRefreshSeconds': autoRefreshSeconds, }; static THomeBase fromMap(Map 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 ?? ''), ); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d29810a7..6a19590a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.macos.yaml b/pubspec.macos.yaml index 5bdb0b05..69e3a01f 100644 --- a/pubspec.macos.yaml +++ b/pubspec.macos.yaml @@ -21,7 +21,7 @@ name: xianyan description: "闲言 — 灵感语录更纯粹。每日拾句 + 壁纸创作 APP" publish_to: 'none' -version: 6.6.6+2606061 +version: 6.6.7+2606071 # 年月日-次 7位 environment: