Files
xianyan/lib/core/providers/split_view_provider.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

345 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 宽屏分屏状态管理
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-18
/// 作用: 管理分屏比例、右侧面板内容、导航栏位置、分屏开关、工作台模式
/// 上次更新: 新增工作台交互增强字段(专注阅读/右栏分屏/拖拽出窗/右栏标签页/中栏拖拽排序/毛玻璃/空状态动画)
/// ============================================================
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:xianyan/core/storage/kv_storage.dart';
import 'package:xianyan/core/constants/default_settings.dart';
part 'split_view_provider.freezed.dart';
/// 导航栏停靠位置
enum NavBarPosition {
left,
right,
top,
bottom;
String get label => switch (this) {
left => '左侧',
right => '右侧',
top => '顶部',
bottom => '底部',
};
String get emoji => switch (this) {
left => '⬅️',
right => '➡️',
top => '⬆️',
bottom => '⬇️',
};
}
/// 分屏比例选项
class SplitRatioOption {
const SplitRatioOption(this.value, this.label);
final double value;
final String label;
static const List<SplitRatioOption> options = [
SplitRatioOption(0.30, '30:70'),
SplitRatioOption(0.35, '35:65'),
SplitRatioOption(0.40, '40:60'),
SplitRatioOption(0.45, '45:55'),
SplitRatioOption(0.50, '50:50'),
SplitRatioOption(0.55, '55:45'),
SplitRatioOption(0.60, '60:40'),
];
static SplitRatioOption? findByValue(double v) =>
options.where((o) => o.value == v).firstOrNull;
static int indexOfValue(double v) {
final idx = options.indexWhere((o) => o.value == v);
return idx >= 0 ? idx : 2;
}
}
const String _kSplitRatio = 'split_view_ratio';
const String _kNavBarPosition = 'nav_bar_position';
const String _kSplitViewEnabled = 'split_view_enabled';
const String _kWorkbenchEnabled = 'workbench_enabled';
const String _kFocusReadingMode = 'workbench_focus_reading';
const String _kRightPanelSplit = 'workbench_right_panel_split';
const String _kPopOutWindow = 'workbench_pop_out_window';
const String _kRightPanelTabs = 'workbench_right_panel_tabs';
const String _kMiddlePanelDragSort = 'workbench_middle_drag_sort';
const String _kWorkbenchBlurBackground = 'workbench_blur_bg';
const String _kEmptyStateAnimation = 'workbench_empty_anim';
const String _kNavBarCollapsed = 'nav_bar_collapsed';
const String _kNavBarWidth = 'workbench_nav_bar_width';
const String _kTabSplitRatios = 'tab_split_ratios';
/// @deprecated 旧三栏断点,保留向后兼容,实际指向 kMediumBreakpoint (1024.0)
/// 新断点常量定义在 adaptive_split_view.dart
const double kTripleColumnBreakpoint = 1024.0;
/// 分屏视图状态freezed不可变数据类
@freezed
sealed class SplitViewState with _$SplitViewState {
const SplitViewState._();
const factory SplitViewState({
@Default(0.4) double splitRatio,
String? rightPanelContent,
Map<String, dynamic>? rightPanelArgs,
@Default(NavBarPosition.left) NavBarPosition navBarPosition,
@Default(false) bool splitViewEnabled,
/// 工作台模式开关(宽屏自动开启,用户可手动关闭)
@Default(true) bool workbenchEnabled,
String? homeRightPanel,
String? discoverRightPanel,
String? profileRightPanel,
@Default(0) int currentTab,
String? thirdPanelContent,
Map<String, dynamic>? thirdPanelArgs,
// === 工作台交互增强P0-8 新增)===
/// 专注阅读模式:隐藏中栏+导航栏,右栏全宽沉浸
@Default(false) bool focusReadingMode,
/// 右栏分屏:超宽屏(≥1920px)时右栏再分屏
@Default(false) bool rightPanelSplit,
/// 拖拽出窗(占位,需多窗口支持)
@Default(false) bool popOutWindow,
/// 右栏标签页(占位,复用 RightPanelStackNotifier 扩展)
@Default(false) bool rightPanelTabs,
/// 中栏拖拽排序
@Default(false) bool middlePanelDragSort,
/// 工作台毛玻璃背景(仅桌面端)
@Default(true) bool workbenchBlurBackground,
/// 空状态动画
@Default(true) bool emptyStateAnimation,
// === 侧边栏折叠 + 分屏记忆2026-06-18 新增)===
/// 导航栏折叠状态(折叠时仅显示图标 48px
@Default(false) bool navBarCollapsed,
/// 导航栏宽度(展开态可拖拽调节,默认 72.0
@Default(72.0) double navBarWidth,
/// 各 Tab 独立分屏比例key=tabIndex, value=ratio
/// 切换 Tab 时恢复该 Tab 上次的分屏比例
@Default({}) Map<int, double> tabSplitRatios,
}) = _SplitViewState;
String? get activeRightPanel => switch (currentTab) {
0 => homeRightPanel,
1 => discoverRightPanel,
2 => profileRightPanel,
_ => null,
};
}
/// 分屏视图状态管理Notifier
class SplitViewNotifier extends Notifier<SplitViewState> {
@override
SplitViewState build() {
final savedIndex = KvStorage.getInt(_kNavBarPosition) ?? 0;
const positions = NavBarPosition.values;
// 读取分屏记忆JSON 序列化的 Map<int, double>
final tabRatiosJson = KvStorage.getString(_kTabSplitRatios);
Map<int, double> tabSplitRatios = {};
if (tabRatiosJson != null && tabRatiosJson.isNotEmpty) {
try {
final decoded = jsonDecode(tabRatiosJson) as Map<String, dynamic>;
tabSplitRatios = decoded.map(
(k, v) => MapEntry(int.parse(k), (v as num).toDouble()),
);
} catch (_) {
// 解析失败使用空 Map
}
}
return SplitViewState(
splitRatio: KvStorage.getDouble(_kSplitRatio) ?? 0.4,
navBarPosition: (savedIndex >= 0 && savedIndex < positions.length)
? positions[savedIndex]
: NavBarPosition.left,
splitViewEnabled:
KvStorage.getBool(_kSplitViewEnabled) ??
DefaultSettings.splitViewEnabled,
workbenchEnabled:
KvStorage.getBool(_kWorkbenchEnabled) ?? true,
focusReadingMode: KvStorage.getBool(_kFocusReadingMode) ?? false,
rightPanelSplit: KvStorage.getBool(_kRightPanelSplit) ?? false,
popOutWindow: KvStorage.getBool(_kPopOutWindow) ?? false,
rightPanelTabs: KvStorage.getBool(_kRightPanelTabs) ?? false,
middlePanelDragSort: KvStorage.getBool(_kMiddlePanelDragSort) ?? false,
workbenchBlurBackground: KvStorage.getBool(_kWorkbenchBlurBackground) ?? true,
emptyStateAnimation: KvStorage.getBool(_kEmptyStateAnimation) ?? true,
navBarCollapsed: KvStorage.getBool(_kNavBarCollapsed) ?? false,
navBarWidth: KvStorage.getDouble(_kNavBarWidth) ?? 72.0,
tabSplitRatios: tabSplitRatios,
);
}
void setSplitRatio(double value) {
KvStorage.setDouble(_kSplitRatio, value);
state = state.copyWith(splitRatio: value);
}
void setRightPanelContent(String? content, {Map<String, dynamic>? args}) {
state = state.copyWith(rightPanelContent: content, rightPanelArgs: args);
}
void setNavBarPosition(NavBarPosition position) {
KvStorage.setInt(_kNavBarPosition, position.index);
state = state.copyWith(navBarPosition: position);
}
void setSplitViewEnabled(bool enabled) {
KvStorage.setBool(_kSplitViewEnabled, enabled);
state = state.copyWith(splitViewEnabled: enabled);
}
/// 设置工作台模式开关(宽屏自动开启,用户可手动关闭)
void setWorkbenchEnabled(bool enabled) {
KvStorage.setBool(_kWorkbenchEnabled, enabled);
state = state.copyWith(workbenchEnabled: enabled);
}
// === 工作台交互增强 setterP0-8 新增)===
/// 设置专注阅读模式
void setFocusReadingMode(bool enabled) {
KvStorage.setBool(_kFocusReadingMode, enabled);
state = state.copyWith(focusReadingMode: enabled);
}
/// 设置右栏分屏
void setRightPanelSplit(bool enabled) {
KvStorage.setBool(_kRightPanelSplit, enabled);
state = state.copyWith(rightPanelSplit: enabled);
}
/// 设置拖拽出窗(占位)
void setPopOutWindow(bool enabled) {
KvStorage.setBool(_kPopOutWindow, enabled);
state = state.copyWith(popOutWindow: enabled);
}
/// 设置右栏标签页(占位)
void setRightPanelTabs(bool enabled) {
KvStorage.setBool(_kRightPanelTabs, enabled);
state = state.copyWith(rightPanelTabs: enabled);
}
/// 设置中栏拖拽排序
void setMiddlePanelDragSort(bool enabled) {
KvStorage.setBool(_kMiddlePanelDragSort, enabled);
state = state.copyWith(middlePanelDragSort: enabled);
}
/// 设置工作台毛玻璃背景
void setWorkbenchBlurBackground(bool enabled) {
KvStorage.setBool(_kWorkbenchBlurBackground, enabled);
state = state.copyWith(workbenchBlurBackground: enabled);
}
/// 设置空状态动画
void setEmptyStateAnimation(bool enabled) {
KvStorage.setBool(_kEmptyStateAnimation, enabled);
state = state.copyWith(emptyStateAnimation: enabled);
}
// === 侧边栏折叠 + 分屏记忆 setter2026-06-18 新增)===
/// 设置导航栏折叠状态
/// 折叠时仅显示图标48px展开时恢复原宽度
void setNavBarCollapsed(bool collapsed) {
KvStorage.setBool(_kNavBarCollapsed, collapsed);
state = state.copyWith(navBarCollapsed: collapsed);
}
/// 切换导航栏折叠状态
void toggleNavBarCollapsed() {
setNavBarCollapsed(!state.navBarCollapsed);
}
/// 设置导航栏宽度(展开态可拖拽调节)
/// 范围限制48.0(最小折叠态)~ 240.0(最大展开态)
void setNavBarWidth(double width) {
final clamped = width.clamp(48.0, 240.0);
KvStorage.setDouble(_kNavBarWidth, clamped);
state = state.copyWith(navBarWidth: clamped);
}
/// 保存当前 Tab 的分屏比例到记忆
void saveCurrentTabSplitRatio() {
final tab = state.currentTab;
final ratio = state.splitRatio;
final newRatios = Map<int, double>.from(state.tabSplitRatios);
newRatios[tab] = ratio;
// JSON 序列化key 必须是 String
final jsonMap = newRatios.map((k, v) => MapEntry(k.toString(), v));
KvStorage.setString(_kTabSplitRatios, jsonEncode(jsonMap));
state = state.copyWith(tabSplitRatios: newRatios);
}
/// 切换 Tab 并恢复该 Tab 上次的分屏比例
void setCurrentTabWithMemory(int index) {
// 先保存当前 Tab 的比例
final currentTab = state.currentTab;
final currentRatio = state.splitRatio;
final newRatios = Map<int, double>.from(state.tabSplitRatios);
newRatios[currentTab] = currentRatio;
// 恢复目标 Tab 的比例(如果有记忆)
final restoredRatio = newRatios[index] ?? 0.4;
// 持久化
final jsonMap = newRatios.map((k, v) => MapEntry(k.toString(), v));
KvStorage.setString(_kTabSplitRatios, jsonEncode(jsonMap));
KvStorage.setDouble(_kSplitRatio, restoredRatio);
state = state.copyWith(
currentTab: index,
splitRatio: restoredRatio,
tabSplitRatios: newRatios,
);
}
void setHomeRightPanel(String? panelId, {Map<String, dynamic>? args}) {
state = state.copyWith(homeRightPanel: panelId, rightPanelArgs: args);
}
void setDiscoverRightPanel(String? panelId, {Map<String, dynamic>? args}) {
state = state.copyWith(discoverRightPanel: panelId, rightPanelArgs: args);
}
void setProfileRightPanel(String? panelId, {Map<String, dynamic>? args}) {
state = state.copyWith(profileRightPanel: panelId, rightPanelArgs: args);
}
void setCurrentTab(int index) {
state = state.copyWith(currentTab: index);
}
/// 设置第三栏面板内容
void setThirdPanel(String? panelId, {Map<String, dynamic>? args}) {
state = state.copyWith(thirdPanelContent: panelId, thirdPanelArgs: args);
}
void clearActivePanel() {
final tab = state.currentTab;
switch (tab) {
case 0:
state = state.copyWith(homeRightPanel: null);
case 1:
state = state.copyWith(discoverRightPanel: null);
case 2:
state = state.copyWith(profileRightPanel: null);
}
}
}
/// 分屏视图Provider
final splitViewProvider = NotifierProvider<SplitViewNotifier, SplitViewState>(
SplitViewNotifier.new,
);