Files
Reboot-Launcher/gui/lib/src/page/implementation/home_page.dart
Alessandro Autiero bfe15e43d9 Released 9.2.7
2024-09-14 12:37:56 +02:00

630 lines
18 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/dll.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
class HomePage extends StatefulWidget {
static const double kDefaultPadding = 12.0;
static const double kTitleBarHeight = 32;
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
final BackendController _backendController = Get.find<BackendController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
final RxBool _focused = RxBool(true);
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
windowManager.setPreventClose(true);
windowManager.addListener(this);
_syncPageViewWithNavigator();
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkUpdates();
_initAppLink();
_checkGameServer();
});
}
void _syncPageViewWithNavigator() {
var lastPage = pageIndex.value;
pageIndex.listen((index) {
if(index == lastPage) {
return;
}
lastPage = index;
_pageController.jumpToPage(index);
});
}
void _initAppLink() async {
final appLinks = AppLinks();
final initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
}
void _joinServer(Uri uri) {
final uuid = uri.host;
final server = _hostingController.findServerById(uuid);
if(server != null) {
_backendController.joinServerInteractive(_hostingController.uuid, server);
}else {
showRebootInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
Future<void> _checkGameServer() async {
try {
final address = _backendController.gameServerAddress.text;
if(isLocalHost(address)) {
return;
}
var result = await pingGameServer(address);
if(result) {
return;
}
_backendController.joinLocalhost();
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
translations.serverNoLongerAvailableUnnamed,
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
));
}catch(_) {
// Intended behaviour
// Just ignore the error
}
}
void _checkUpdates() {
_settingsController.notifyLauncherUpdate();
if(!dllsDirectory.existsSync()) {
dllsDirectory.createSync(recursive: true);
}
for(final injectable in InjectableDll.values) {
final (file, custom) = _dllController.getInjectableData(injectable);
if(!custom) {
_dllController.downloadCriticalDllInteractive(
file.path,
silent: true
);
}
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
_dllController.downloadCriticalDllInteractive(filePath);
}));
}
@override
void onWindowClose() async {
try {
await _hostingController.discardServer();
}finally {
exit(0); // Force closing
}
}
@override
void dispose() {
_searchFocusNode.dispose();
_searchController.dispose();
pagesController.close();
windowManager.removeListener(this);
super.dispose();
}
@override
void onWindowFocus() {
_focused.value = true;
}
@override
void onWindowBlur() {
_focused.value = !_focused.value;
}
@override
void onWindowDocked() {
_focused.value = true;
}
@override
void onWindowMaximize() {
_focused.value = true;
}
@override
void onWindowMinimize() {
_focused.value = false;
}
@override
void onWindowResize() {
_focused.value = true;
}
@override
void onWindowMove() {
_focused.value = true;
}
@override
void onWindowRestore() {
_focused.value = true;
}
@override
void onWindowUndocked() {
_focused.value = true;
}
@override
void onWindowUnmaximize() {
_focused.value = true;
}
@override
void onWindowResized() {
_settingsController.saveWindowSize(appWindow.size);
_focused.value = true;
}
@override
void onWindowMoved() {
_settingsController.saveWindowOffset(appWindow.position);
_focused.value = true;
}
@override
void onWindowEnterFullScreen() {
_focused.value = true;
}
@override
void onWindowLeaveFullScreen() {
_focused.value = true;
}
@override
void onWindowEvent(String eventName) {
if(eventName != "move") {
WidgetsBinding.instance.addPostFrameCallback((_) => log("[WINDOW] Event: $eventName ${_focused.value}"));
}
}
@override
Widget build(BuildContext context) {
super.build(context);
_settingsController.language.value;
loadTranslations(context);
return Obx(() {
return Container(
color: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: HomePage.kTitleBarHeight,
child: Row(
children: [
_backButton,
Expanded(child: _draggableArea),
WindowTitleBar(focused: _focused())
],
)
),
Expanded(
child: Navigator(
key: appNavigatorKey,
onPopPage: (page, data) => false,
pages: [
MaterialPage(
child: Overlay(
key: appOverlayKey,
initialEntries: [
OverlayEntry(
maintainState: true,
builder: (context) => Row(
children: [
_buildLateralView(),
_buildBody()
],
)
)
],
),
)
],
)
)
],
),
);
});
}
Widget _buildBody() => Expanded(
child: Padding(
padding: EdgeInsets.only(
left: HomePage.kDefaultPadding,
right: HomePage.kDefaultPadding * 2,
top: HomePage.kDefaultPadding,
bottom: HomePage.kDefaultPadding * 2
),
child: Column(
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 1000
),
child: Center(
child: Column(
children: [
_buildBodyHeader(),
const SizedBox(height: 24.0),
Expanded(
child: Stack(
fit: StackFit.loose,
children: [
_buildBodyContent(),
InfoBarArea(
key: infoBarAreaKey
)
],
)
),
],
),
),
),
)
],
)
),
);
Widget _buildBodyContent() => PageView.builder(
controller: _pageController,
itemBuilder: (context, index) => Navigator(
onPopPage: (page, data) => true,
observers: [
_NestedPageObserver(
onChanged: (routeName) {
if(routeName != null) {
pageIndex.refresh();
addSubPageToStack(routeName);
pagesController.add(null);
}
}
)
],
pages: [
MaterialPage(
child: KeyedSubtree(
key: getPageKeyByIndex(index),
child: pages[index]
)
)
],
),
itemCount: pages.length
);
Widget _buildBodyHeader() {
final themeMode = _settingsController.themeMode.value;
final inactiveColor = themeMode == ThemeMode.dark
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
return Align(
alignment: Alignment.centerLeft,
child: StreamBuilder(
stream: pagesController.stream,
builder: (context, _) {
final elements = <TextSpan>[];
elements.add(_buildBodyHeaderRootPage(inactiveColor));
for(var i = pageStack.length - 1; i >= 0; i--) {
var innerPage = pageStack.elementAt(i);
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
elements.add(_buildBodyHeaderPageSeparator(inactiveColor));
elements.add(_buildBodyHeaderNestedPage(innerPage, i, inactiveColor));
}
return Text.rich(
TextSpan(
children: elements
),
style: TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.w600
),
);
}
),
);
}
TextSpan _buildBodyHeaderRootPage(Color inactiveColor) => TextSpan(
text: pages[pageIndex.value].name,
recognizer: pageStack.isNotEmpty ? (TapGestureRecognizer()..onTap = () {
if(inDialog) {
return;
}
for(var i = 0; i < pageStack.length; i++) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
}
pagesController.add(null);
}) : null,
style: TextStyle(
color: pageStack.isNotEmpty ? inactiveColor : null
)
);
TextSpan _buildBodyHeaderPageSeparator(Color inactiveColor) => TextSpan(
text: " > ",
style: TextStyle(
color: inactiveColor
)
);
TextSpan _buildBodyHeaderNestedPage(String nestedPageName, int nestedPageIndex, Color inactiveColor) => TextSpan(
text: nestedPageName,
recognizer: nestedPageIndex == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
if(inDialog) {
return;
}
for(var j = 0; j < nestedPageIndex - 1; j++) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
}
pagesController.add(null);
}),
style: TextStyle(
color: nestedPageIndex == pageStack.length - 1 ? null : inactiveColor
)
);
Widget _buildLateralView() => SizedBox(
width: 310,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfileWidget(
overlayKey: profileOverlayKey
),
_autoSuggestBox,
const SizedBox(height: 12.0),
_buildNavigationTrail()
],
),
);
Widget _buildNavigationTrail() => Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0
),
child: Scrollbar(
child: ListView.separated(
primary: true,
itemCount: pages.length,
separatorBuilder: (context, index) => const SizedBox(
height: 4.0
),
itemBuilder: (context, index) => _buildNavigationItem(pages[index]),
),
),
)
);
Widget _buildNavigationItem(RebootPage page) {
final index = page.type.index;
return OverlayTarget(
key: getOverlayTargetKeyByPage(index),
child: HoverButton(
onPressed: () {
final lastPageIndex = pageIndex.value;
if(lastPageIndex != index) {
pageIndex.value = index;
}else if(pageStack.isNotEmpty) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
pagesController.add(null);
}
},
builder: (context, states) => Obx(() => Container(
height: 36,
decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context),
pageIndex.value == index ? {ButtonStates.hovering} : states,
transparentWhenNone: true,
),
borderRadius: BorderRadius.all(Radius.circular(6.0))
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0
),
child: Row(
children: [
SizedBox.square(
dimension: 24,
child: Image.asset(page.iconAsset)
),
const SizedBox(width: 12.0),
Text(page.name)
],
),
),
)),
),
);
}
Widget get _backButton => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Button(
style: ButtonStyle(
padding: ButtonState.all(const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 16.0
)),
backgroundColor: ButtonState.all(Colors.transparent),
shape: ButtonState.all(Border())
),
onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) {
Navigator.of(appNavigatorKey.currentContext!).pop();
}else {
final lastPage = appStack.removeLast();
pageStack.remove(lastPage);
if (lastPage is int) {
hitBack = true;
pageIndex.value = lastPage;
} else {
Navigator.of(pageKey.currentContext!).pop();
}
}
pagesController.add(null);
},
child: const Icon(FluentIcons.back, size: 12.0),
)
);
GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: appWindow.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging()
);
Widget get _autoSuggestBox => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0
),
child: AutoSuggestBox<PageSuggestion>(
key: _searchKey,
controller: _searchController,
placeholder: translations.find,
focusNode: _searchFocusNode,
selectionHeightStyle: BoxHeightStyle.max,
itemBuilder: (context, item) => ListTile(
onPressed: () {
pageIndex.value = item.value.pageIndex;
_searchController.clear();
_searchFocusNode.unfocus();
},
leading: item.child,
title: Text(
item.value.name,
overflow: TextOverflow.clip,
maxLines: 1
)
),
items: _suggestedItems,
autofocus: true,
trailingIcon: IgnorePointer(
child: IconButton(
onPressed: () {},
icon: Transform.flip(
flipX: true,
child: const Icon(FluentIcons.search)
),
)
),
)
);
List<AutoSuggestBoxItem<PageSuggestion>> get _suggestedItems => pages.mapMany((page) {
final pageIcon = SizedBox.square(
dimension: 24,
child: Image.asset(page.iconAsset)
);
final results = <AutoSuggestBoxItem<PageSuggestion>>[];
results.add(AutoSuggestBoxItem(
value: PageSuggestion(
name: page.name,
description: "",
pageIndex: page.index
),
label: page.name,
child: pageIcon
));
return results;
}).toList();
}
class _NestedPageObserver extends NavigatorObserver {
final void Function(String?) onChanged;
_NestedPageObserver({required this.onChanged});
@override
void didPush(Route route, Route? previousRoute) {
if(previousRoute != null) {
WidgetsBinding.instance.addPostFrameCallback((_) => onChanged(route.settings.name));
}
}
}