1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
345 lines
12 KiB
Dart
345 lines
12 KiB
Dart
/// ============================================================
|
||
/// 闲言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);
|
||
}
|
||
|
||
// === 工作台交互增强 setter(P0-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);
|
||
}
|
||
|
||
// === 侧边栏折叠 + 分屏记忆 setter(2026-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,
|
||
);
|