commit
54c5b6df1c
@ -20,28 +20,28 @@ class ConnectionTabPage extends StatefulWidget {
|
|||||||
State<ConnectionTabPage> createState() => _ConnectionTabPageState(params);
|
State<ConnectionTabPage> createState() => _ConnectionTabPageState(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConnectionTabPageState extends State<ConnectionTabPage>
|
class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
// refactor List<int> when using multi-tab
|
// refactor List<int> when using multi-tab
|
||||||
// this singleton is only for test
|
// this singleton is only for test
|
||||||
RxList<TabInfo> tabs = RxList<TabInfo>.empty(growable: true);
|
RxList<TabInfo> tabs = RxList<TabInfo>.empty(growable: true);
|
||||||
late Rx<TabController> tabController;
|
|
||||||
static final Rx<int> _selected = 0.obs;
|
|
||||||
static final Rx<String> _fullscreenID = "".obs;
|
static final Rx<String> _fullscreenID = "".obs;
|
||||||
IconData icon = Icons.desktop_windows_sharp;
|
final IconData selectedIcon = Icons.desktop_windows_sharp;
|
||||||
|
final IconData unselectedIcon = Icons.desktop_windows_outlined;
|
||||||
|
|
||||||
var connectionMap = RxList<Widget>.empty(growable: true);
|
var connectionMap = RxList<Widget>.empty(growable: true);
|
||||||
|
|
||||||
_ConnectionTabPageState(Map<String, dynamic> params) {
|
_ConnectionTabPageState(Map<String, dynamic> params) {
|
||||||
if (params['id'] != null) {
|
if (params['id'] != null) {
|
||||||
tabs.add(TabInfo(label: params['id'], icon: icon));
|
tabs.add(TabInfo(
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
tabController = TabController(length: tabs.length, vsync: this).obs;
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
print(
|
||||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
@ -50,8 +50,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage>
|
|||||||
final args = jsonDecode(call.arguments);
|
final args = jsonDecode(call.arguments);
|
||||||
final id = args['id'];
|
final id = args['id'];
|
||||||
window_on_top(windowId());
|
window_on_top(windowId());
|
||||||
DesktopTabBar.onAdd(this, tabController, tabs, _selected,
|
DesktopTabBar.onAdd(
|
||||||
TabInfo(label: id, icon: icon));
|
tabs,
|
||||||
|
TabInfo(
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
print(
|
print(
|
||||||
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}");
|
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}");
|
||||||
@ -74,18 +78,16 @@ class _ConnectionTabPageState extends State<ConnectionTabPage>
|
|||||||
Obx(() => Visibility(
|
Obx(() => Visibility(
|
||||||
visible: _fullscreenID.value.isEmpty,
|
visible: _fullscreenID.value.isEmpty,
|
||||||
child: DesktopTabBar(
|
child: DesktopTabBar(
|
||||||
controller: tabController,
|
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
onTabClose: onRemoveId,
|
onTabClose: onRemoveId,
|
||||||
selected: _selected,
|
|
||||||
dark: isDarkTheme(),
|
dark: isDarkTheme(),
|
||||||
mainTab: false,
|
mainTab: false,
|
||||||
))),
|
))),
|
||||||
Expanded(child: Obx(() {
|
Expanded(child: Obx(() {
|
||||||
WindowController.fromWindowId(windowId())
|
WindowController.fromWindowId(windowId())
|
||||||
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
.setFullscreen(_fullscreenID.value.isNotEmpty);
|
||||||
return TabBarView(
|
return PageView(
|
||||||
controller: tabController.value,
|
controller: DesktopTabBar.controller.value,
|
||||||
children: tabs
|
children: tabs
|
||||||
.map((tab) => RemotePage(
|
.map((tab) => RemotePage(
|
||||||
key: ValueKey(tab.label),
|
key: ValueKey(tab.label),
|
||||||
@ -103,7 +105,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onRemoveId(String id) {
|
void onRemoveId(String id) {
|
||||||
DesktopTabBar.onClose(this, tabController, tabs, id);
|
|
||||||
ffi(id).close();
|
ffi(id).close();
|
||||||
if (tabs.length == 0) {
|
if (tabs.length == 0) {
|
||||||
WindowController.fromWindowId(windowId()).close();
|
WindowController.fromWindowId(windowId()).close();
|
||||||
|
@ -13,20 +13,19 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DesktopTabPageState extends State<DesktopTabPage>
|
class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late Rx<TabController> tabController;
|
|
||||||
late RxList<TabInfo> tabs;
|
late RxList<TabInfo> tabs;
|
||||||
static final Rx<int> _selected = 0.obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
tabs = RxList.from([
|
tabs = RxList.from([
|
||||||
TabInfo(label: kTabLabelHomePage, icon: Icons.home_sharp, closable: false)
|
TabInfo(
|
||||||
|
label: kTabLabelHomePage,
|
||||||
|
selectedIcon: Icons.home_sharp,
|
||||||
|
unselectedIcon: Icons.home_outlined,
|
||||||
|
closable: false)
|
||||||
], growable: true);
|
], growable: true);
|
||||||
tabController =
|
|
||||||
TabController(length: tabs.length, vsync: this, initialIndex: 0).obs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,17 +34,14 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
DesktopTabBar(
|
DesktopTabBar(
|
||||||
controller: tabController,
|
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
onTabClose: onTabClose,
|
|
||||||
selected: _selected,
|
|
||||||
dark: isDarkTheme(),
|
dark: isDarkTheme(),
|
||||||
mainTab: true,
|
mainTab: true,
|
||||||
onAddSetting: onAddSetting,
|
onAddSetting: onAddSetting,
|
||||||
),
|
),
|
||||||
Obx((() => Expanded(
|
Obx((() => Expanded(
|
||||||
child: TabBarView(
|
child: PageView(
|
||||||
controller: tabController.value,
|
controller: DesktopTabBar.controller.value,
|
||||||
children: tabs.map((tab) {
|
children: tabs.map((tab) {
|
||||||
switch (tab.label) {
|
switch (tab.label) {
|
||||||
case kTabLabelHomePage:
|
case kTabLabelHomePage:
|
||||||
@ -62,12 +58,12 @@ class _DesktopTabPageState extends State<DesktopTabPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTabClose(String label) {
|
|
||||||
DesktopTabBar.onClose(this, tabController, tabs, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddSetting() {
|
void onAddSetting() {
|
||||||
DesktopTabBar.onAdd(this, tabController, tabs, _selected,
|
DesktopTabBar.onAdd(
|
||||||
TabInfo(label: kTabLabelSettingPage, icon: Icons.build));
|
tabs,
|
||||||
|
TabInfo(
|
||||||
|
label: kTabLabelSettingPage,
|
||||||
|
selectedIcon: Icons.build_sharp,
|
||||||
|
unselectedIcon: Icons.build_outlined));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,25 +19,25 @@ class FileManagerTabPage extends StatefulWidget {
|
|||||||
State<FileManagerTabPage> createState() => _FileManagerTabPageState(params);
|
State<FileManagerTabPage> createState() => _FileManagerTabPageState(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FileManagerTabPageState extends State<FileManagerTabPage>
|
class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
// refactor List<int> when using multi-tab
|
// refactor List<int> when using multi-tab
|
||||||
// this singleton is only for test
|
// this singleton is only for test
|
||||||
RxList<TabInfo> tabs = List<TabInfo>.empty(growable: true).obs;
|
RxList<TabInfo> tabs = List<TabInfo>.empty(growable: true).obs;
|
||||||
late Rx<TabController> tabController;
|
final IconData selectedIcon = Icons.file_copy_sharp;
|
||||||
static final Rx<int> _selected = 0.obs;
|
final IconData unselectedIcon = Icons.file_copy_outlined;
|
||||||
IconData icon = Icons.file_copy_sharp;
|
|
||||||
|
|
||||||
_FileManagerTabPageState(Map<String, dynamic> params) {
|
_FileManagerTabPageState(Map<String, dynamic> params) {
|
||||||
if (params['id'] != null) {
|
if (params['id'] != null) {
|
||||||
tabs.add(TabInfo(label: params['id'], icon: icon));
|
tabs.add(TabInfo(
|
||||||
|
label: params['id'],
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
tabController = TabController(length: tabs.length, vsync: this).obs;
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
||||||
print(
|
print(
|
||||||
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
"call ${call.method} with args ${call.arguments} from window ${fromWindowId}");
|
||||||
@ -46,8 +46,12 @@ class _FileManagerTabPageState extends State<FileManagerTabPage>
|
|||||||
final args = jsonDecode(call.arguments);
|
final args = jsonDecode(call.arguments);
|
||||||
final id = args['id'];
|
final id = args['id'];
|
||||||
window_on_top(windowId());
|
window_on_top(windowId());
|
||||||
DesktopTabBar.onAdd(this, tabController, tabs, _selected,
|
DesktopTabBar.onAdd(
|
||||||
TabInfo(label: id, icon: icon));
|
tabs,
|
||||||
|
TabInfo(
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon));
|
||||||
} else if (call.method == "onDestroy") {
|
} else if (call.method == "onDestroy") {
|
||||||
print(
|
print(
|
||||||
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}");
|
"executing onDestroy hook, closing ${tabs.map((tab) => tab.label).toList()}");
|
||||||
@ -68,17 +72,15 @@ class _FileManagerTabPageState extends State<FileManagerTabPage>
|
|||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
DesktopTabBar(
|
DesktopTabBar(
|
||||||
controller: tabController,
|
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
onTabClose: onRemoveId,
|
onTabClose: onRemoveId,
|
||||||
selected: _selected,
|
|
||||||
dark: isDarkTheme(),
|
dark: isDarkTheme(),
|
||||||
mainTab: false,
|
mainTab: false,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(
|
child: Obx(
|
||||||
() => TabBarView(
|
() => PageView(
|
||||||
controller: tabController.value,
|
controller: DesktopTabBar.controller.value,
|
||||||
children: tabs
|
children: tabs
|
||||||
.map((tab) => FileManagerPage(
|
.map((tab) => FileManagerPage(
|
||||||
key: ValueKey(tab.label),
|
key: ValueKey(tab.label),
|
||||||
@ -92,8 +94,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onRemoveId(String id) {
|
void onRemoveId(String id) {
|
||||||
DesktopTabBar.onClose(this, tabController, tabs, id);
|
ffi("ft_$id").close();
|
||||||
ffi(id).close();
|
|
||||||
if (tabs.length == 0) {
|
if (tabs.length == 0) {
|
||||||
WindowController.fromWindowId(windowId()).close();
|
WindowController.fromWindowId(windowId()).close();
|
||||||
}
|
}
|
||||||
|
@ -8,21 +8,22 @@ import 'package:flutter_hbb/main.dart';
|
|||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:scroll_pos/scroll_pos.dart';
|
||||||
|
|
||||||
const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
|
const double _kTabBarHeight = kDesktopRemoteTabBarHeight;
|
||||||
const double _kIconSize = 18;
|
const double _kIconSize = 18;
|
||||||
const double _kDividerIndent = 10;
|
const double _kDividerIndent = 10;
|
||||||
const double _kAddIconSize = _kTabBarHeight - 15;
|
const double _kAddIconSize = _kTabBarHeight - 15;
|
||||||
final tabBarKey = GlobalKey();
|
final _tabBarKey = GlobalKey();
|
||||||
|
|
||||||
void closeTab(String? id) {
|
void closeTab(String? id) {
|
||||||
final tabBar = tabBarKey.currentWidget as TabBar?;
|
final tabBar = _tabBarKey.currentWidget as _ListView?;
|
||||||
if (tabBar == null) return;
|
if (tabBar == null) return;
|
||||||
final tabs = tabBar.tabs as List<_Tab>;
|
final tabs = tabBar.tabs;
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
final current = tabBar.controller?.index;
|
if (tabBar.selected.value < tabs.length) {
|
||||||
if (current == null) return;
|
tabs[tabBar.selected.value].onClose();
|
||||||
tabs[current].onClose();
|
}
|
||||||
} else {
|
} else {
|
||||||
for (final tab in tabs) {
|
for (final tab in tabs) {
|
||||||
if (tab.label == id) {
|
if (tab.label == id) {
|
||||||
@ -35,33 +36,45 @@ void closeTab(String? id) {
|
|||||||
|
|
||||||
class TabInfo {
|
class TabInfo {
|
||||||
late final String label;
|
late final String label;
|
||||||
late final IconData icon;
|
late final IconData selectedIcon;
|
||||||
|
late final IconData unselectedIcon;
|
||||||
late final bool closable;
|
late final bool closable;
|
||||||
|
|
||||||
TabInfo({required this.label, required this.icon, this.closable = true});
|
TabInfo(
|
||||||
|
{required this.label,
|
||||||
|
required this.selectedIcon,
|
||||||
|
required this.unselectedIcon,
|
||||||
|
this.closable = true});
|
||||||
}
|
}
|
||||||
|
|
||||||
class DesktopTabBar extends StatelessWidget {
|
class DesktopTabBar extends StatelessWidget {
|
||||||
late final Rx<TabController> controller;
|
|
||||||
late final RxList<TabInfo> tabs;
|
late final RxList<TabInfo> tabs;
|
||||||
late final Function(String) onTabClose;
|
late final Function(String)? onTabClose;
|
||||||
late final Rx<int> selected;
|
|
||||||
late final bool dark;
|
late final bool dark;
|
||||||
late final _Theme _theme;
|
late final _Theme _theme;
|
||||||
late final bool mainTab;
|
late final bool mainTab;
|
||||||
late final Function()? onAddSetting;
|
late final Function()? onAddSetting;
|
||||||
|
final ScrollPosController scrollController =
|
||||||
|
ScrollPosController(itemCount: 0);
|
||||||
|
static final Rx<PageController> controller = PageController().obs;
|
||||||
|
static final Rx<int> selected = 0.obs;
|
||||||
|
|
||||||
DesktopTabBar({
|
DesktopTabBar({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.controller,
|
|
||||||
required this.tabs,
|
required this.tabs,
|
||||||
required this.onTabClose,
|
this.onTabClose,
|
||||||
required this.selected,
|
|
||||||
required this.dark,
|
required this.dark,
|
||||||
required this.mainTab,
|
required this.mainTab,
|
||||||
this.onAddSetting,
|
this.onAddSetting,
|
||||||
}) : _theme = dark ? _Theme.dark() : _Theme.light(),
|
}) : _theme = dark ? _Theme.dark() : _Theme.light(),
|
||||||
super(key: key);
|
super(key: key) {
|
||||||
|
scrollController.itemCount = tabs.length;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
debugPrint("callback");
|
||||||
|
scrollController.scrollToItem(selected.value,
|
||||||
|
center: true, animate: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -89,49 +102,21 @@ class DesktopTabBar extends StatelessWidget {
|
|||||||
.startDragging();
|
.startDragging();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Obx(() => TabBar(
|
child: _ListView(
|
||||||
key: tabBarKey,
|
key: _tabBarKey,
|
||||||
indicatorColor: _theme.indicatorColor,
|
controller: controller,
|
||||||
labelPadding: const EdgeInsets.symmetric(
|
scrollController: scrollController,
|
||||||
vertical: 0, horizontal: 0),
|
tabInfos: tabs,
|
||||||
isScrollable: true,
|
selected: selected,
|
||||||
indicatorPadding: EdgeInsets.zero,
|
onTabClose: onTabClose,
|
||||||
physics: BouncingScrollPhysics(),
|
theme: _theme)),
|
||||||
controller: controller.value,
|
|
||||||
tabs: tabs.asMap().entries.map((e) {
|
|
||||||
int index = e.key;
|
|
||||||
String label = e.value.label;
|
|
||||||
|
|
||||||
return _Tab(
|
|
||||||
index: index,
|
|
||||||
label: label,
|
|
||||||
icon: e.value.icon,
|
|
||||||
closable: e.value.closable,
|
|
||||||
selected: selected.value,
|
|
||||||
onClose: () {
|
|
||||||
onTabClose(label);
|
|
||||||
if (index <= selected.value) {
|
|
||||||
selected.value = max(0, selected.value - 1);
|
|
||||||
}
|
|
||||||
controller.value.animateTo(selected.value,
|
|
||||||
duration: Duration.zero);
|
|
||||||
},
|
|
||||||
onSelected: () {
|
|
||||||
selected.value = index;
|
|
||||||
controller.value
|
|
||||||
.animateTo(index, duration: Duration.zero);
|
|
||||||
},
|
|
||||||
theme: _theme,
|
|
||||||
);
|
|
||||||
}).toList())),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: mainTab,
|
offstage: mainTab,
|
||||||
child: _AddButton(
|
child: _AddButton(
|
||||||
theme: _theme,
|
theme: _theme,
|
||||||
).paddingOnly(left: 10),
|
).paddingOnly(left: 10),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -157,32 +142,16 @@ class DesktopTabBar extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static onClose(
|
static onAdd(RxList<TabInfo> tabs, TabInfo tab) {
|
||||||
TickerProvider vsync,
|
|
||||||
Rx<TabController> controller,
|
|
||||||
RxList<TabInfo> tabs,
|
|
||||||
String label,
|
|
||||||
) {
|
|
||||||
tabs.removeWhere((tab) => tab.label == label);
|
|
||||||
controller.value = TabController(
|
|
||||||
length: tabs.length,
|
|
||||||
vsync: vsync,
|
|
||||||
initialIndex: max(0, tabs.length - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
static onAdd(TickerProvider vsync, Rx<TabController> controller,
|
|
||||||
RxList<TabInfo> tabs, Rx<int> selected, TabInfo tab) {
|
|
||||||
int index = tabs.indexWhere((e) => e.label == tab.label);
|
int index = tabs.indexWhere((e) => e.label == tab.label);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
controller.value.animateTo(index, duration: Duration.zero);
|
|
||||||
selected.value = index;
|
selected.value = index;
|
||||||
} else {
|
} else {
|
||||||
tabs.add(tab);
|
tabs.add(tab);
|
||||||
controller.value = TabController(
|
|
||||||
length: tabs.length, vsync: vsync, initialIndex: tabs.length - 1);
|
|
||||||
controller.value.animateTo(tabs.length - 1, duration: Duration.zero);
|
|
||||||
selected.value = tabs.length - 1;
|
selected.value = tabs.length - 1;
|
||||||
|
assert(selected.value >= 0);
|
||||||
}
|
}
|
||||||
|
controller.value.jumpToPage(selected.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,10 +234,76 @@ class WindowActionPanel extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ListView extends StatelessWidget {
|
||||||
|
late Rx<PageController> controller;
|
||||||
|
final ScrollPosController scrollController;
|
||||||
|
final RxList<TabInfo> tabInfos;
|
||||||
|
final Rx<int> selected;
|
||||||
|
final Function(String label)? onTabClose;
|
||||||
|
final _Theme _theme;
|
||||||
|
late List<_Tab> tabs;
|
||||||
|
|
||||||
|
_ListView({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.tabInfos,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTabClose,
|
||||||
|
required _Theme theme,
|
||||||
|
}) : _theme = theme,
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
tabs = tabInfos.asMap().entries.map((e) {
|
||||||
|
int index = e.key;
|
||||||
|
String label = e.value.label;
|
||||||
|
return _Tab(
|
||||||
|
index: index,
|
||||||
|
label: label,
|
||||||
|
selectedIcon: e.value.selectedIcon,
|
||||||
|
unselectedIcon: e.value.unselectedIcon,
|
||||||
|
closable: e.value.closable,
|
||||||
|
selected: selected.value,
|
||||||
|
onClose: () {
|
||||||
|
tabInfos.removeWhere((tab) => tab.label == label);
|
||||||
|
onTabClose?.call(label);
|
||||||
|
if (index <= selected.value) {
|
||||||
|
selected.value = max(0, selected.value - 1);
|
||||||
|
}
|
||||||
|
assert(tabInfos.length == 0 || selected.value < tabInfos.length);
|
||||||
|
scrollController.itemCount = tabInfos.length;
|
||||||
|
if (tabInfos.length > 0) {
|
||||||
|
scrollController.scrollToItem(selected.value,
|
||||||
|
center: true, animate: true);
|
||||||
|
controller.value.jumpToPage(selected.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelected: () {
|
||||||
|
selected.value = index;
|
||||||
|
scrollController.scrollToItem(index, center: true, animate: true);
|
||||||
|
controller.value.jumpToPage(index);
|
||||||
|
},
|
||||||
|
theme: _theme,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
return ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: BouncingScrollPhysics(),
|
||||||
|
children: tabs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _Tab extends StatelessWidget {
|
class _Tab extends StatelessWidget {
|
||||||
late final int index;
|
late final int index;
|
||||||
late final String label;
|
late final String label;
|
||||||
late final IconData icon;
|
late final IconData selectedIcon;
|
||||||
|
late final IconData unselectedIcon;
|
||||||
late final bool closable;
|
late final bool closable;
|
||||||
late final int selected;
|
late final int selected;
|
||||||
late final Function() onClose;
|
late final Function() onClose;
|
||||||
@ -280,7 +315,8 @@ class _Tab extends StatelessWidget {
|
|||||||
{Key? key,
|
{Key? key,
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.icon,
|
required this.selectedIcon,
|
||||||
|
required this.unselectedIcon,
|
||||||
required this.closable,
|
required this.closable,
|
||||||
required this.selected,
|
required this.selected,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
@ -292,13 +328,16 @@ class _Tab extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool is_selected = index == selected;
|
bool is_selected = index == selected;
|
||||||
bool show_divider = index != selected - 1 && index != selected;
|
bool show_divider = index != selected - 1 && index != selected;
|
||||||
return Ink(
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Ink(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onHover: (hover) => _hover.value = hover,
|
onHover: (hover) => _hover.value = hover,
|
||||||
onTap: () => onSelected(),
|
onTap: () => onSelected(),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Tab(
|
Container(
|
||||||
|
height: _kTabBarHeight,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@ -306,7 +345,7 @@ class _Tab extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
is_selected ? selectedIcon : unselectedIcon,
|
||||||
size: _kIconSize,
|
size: _kIconSize,
|
||||||
color: is_selected
|
color: is_selected
|
||||||
? theme.selectedtabIconColor
|
? theme.selectedtabIconColor
|
||||||
@ -345,6 +384,18 @@ class _Tab extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
height: 2,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
is_selected ? theme.indicatorColor : Colors.transparent),
|
||||||
|
))
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ packages:
|
|||||||
name: async
|
name: async
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.2"
|
version: "2.9.0"
|
||||||
back_button_interceptor:
|
back_button_interceptor:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -147,7 +147,7 @@ packages:
|
|||||||
name: characters
|
name: characters
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.1"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -168,7 +168,7 @@ packages:
|
|||||||
name: clock
|
name: clock
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.1"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -235,15 +235,17 @@ packages:
|
|||||||
dash_chat_2:
|
dash_chat_2:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dash_chat_2
|
path: "."
|
||||||
url: "https://pub.flutter-io.cn"
|
ref: feat_maxWidth
|
||||||
source: hosted
|
resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a"
|
||||||
|
url: "https://github.com/fufesou/Dash-Chat-2"
|
||||||
|
source: git
|
||||||
version: "0.0.12"
|
version: "0.0.12"
|
||||||
desktop_drop:
|
desktop_drop:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: desktop_drop
|
name: desktop_drop
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.3"
|
version: "0.3.3"
|
||||||
desktop_multi_window:
|
desktop_multi_window:
|
||||||
@ -324,7 +326,7 @@ packages:
|
|||||||
name: fake_async
|
name: fake_async
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -607,14 +609,14 @@ packages:
|
|||||||
name: matcher
|
name: matcher
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.11"
|
version: "0.12.12"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
version: "0.1.5"
|
||||||
menu_base:
|
menu_base:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -628,7 +630,7 @@ packages:
|
|||||||
name: meta
|
name: meta
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.7.0"
|
version: "1.8.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -705,7 +707,7 @@ packages:
|
|||||||
name: path
|
name: path
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.2"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -846,6 +848,13 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.2"
|
version: "0.1.2"
|
||||||
|
scroll_pos:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: scroll_pos
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
settings_ui:
|
settings_ui:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -948,7 +957,7 @@ packages:
|
|||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.9.0"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -990,7 +999,7 @@ packages:
|
|||||||
name: string_scanner
|
name: string_scanner
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1004,14 +1013,14 @@ packages:
|
|||||||
name: term_glyph
|
name: term_glyph
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.1"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.9"
|
version: "0.4.12"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1029,10 +1038,12 @@ packages:
|
|||||||
tray_manager:
|
tray_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: tray_manager
|
path: "."
|
||||||
url: "https://pub.flutter-io.cn"
|
ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a"
|
||||||
source: hosted
|
resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a"
|
||||||
version: "0.1.7"
|
url: "https://github.com/Kingtous/rustdesk_tray_manager"
|
||||||
|
source: git
|
||||||
|
version: "0.1.8"
|
||||||
tuple:
|
tuple:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -76,6 +76,7 @@ dependencies:
|
|||||||
visibility_detector: ^0.3.3
|
visibility_detector: ^0.3.3
|
||||||
contextmenu: ^3.0.0
|
contextmenu: ^3.0.0
|
||||||
desktop_drop: ^0.3.3
|
desktop_drop: ^0.3.3
|
||||||
|
scroll_pos: ^0.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_launcher_icons: ^0.9.1
|
flutter_launcher_icons: ^0.9.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user