mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
Release 9.2.0
This commit is contained in:
@@ -6,7 +6,9 @@ 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/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.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/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/keyboard.dart';
|
||||
@@ -16,7 +18,10 @@ import 'package:reboot_launcher/src/widget/server_type_selector.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../dialog/implementation/data.dart';
|
||||
final GlobalKey<OverlayTargetState> backendTypeOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendGameServerAddressOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendUnrealEngineOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> backendDetachedOverlayTargetKey = GlobalKey();
|
||||
|
||||
class BackendPage extends RebootPage {
|
||||
const BackendPage({Key? key}) : super(key: key);
|
||||
@@ -42,7 +47,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
InfoBarEntry? _infoBarEntry;
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ServicesBinding.instance.keyboard.addHandler((keyEvent) {
|
||||
@@ -60,7 +65,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_type,
|
||||
@@ -84,10 +89,13 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.matchmakerConfigurationAddressName),
|
||||
subtitle: Text(translations.matchmakerConfigurationAddressDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
content: OverlayTarget(
|
||||
key: backendGameServerAddressOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.matchmakerConfigurationAddressName,
|
||||
controller: _backendController.gameServerAddress,
|
||||
focusNode: _backendController.gameServerAddressFocusNode
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -152,15 +160,18 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
OverlayTarget(
|
||||
key: backendDetachedOverlayTargetKey,
|
||||
child: ToggleSwitch(
|
||||
checked: _backendController.detached(),
|
||||
onChanged: (value) => _backendController.detached.value = value
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Widget get _unrealEngineConsoleKey => Obx(() {
|
||||
if(_backendController.type.value != ServerType.embedded) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -173,14 +184,18 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
title: Text(translations.settingsClientConsoleKeyName),
|
||||
subtitle: Text(translations.settingsClientConsoleKeyDescription),
|
||||
contentWidth: null,
|
||||
content: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
content: OverlayTarget(
|
||||
key: backendUnrealEngineOverlayTargetKey,
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
_infoBarEntry = showRebootInfoBar(
|
||||
translations.clickKey,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
},
|
||||
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -221,7 +236,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
|
||||
),
|
||||
title: Text(translations.backendTypeName),
|
||||
subtitle: Text(translations.backendTypeDescription),
|
||||
content: const ServerTypeSelector()
|
||||
content: ServerTypeSelector(
|
||||
overlayKey: backendTypeOverlayTargetKey
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
359
gui/lib/src/page/implementation/browser_page.dart
Normal file
@@ -0,0 +1,359 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart' as fluentUiIcons;
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.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_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
final Rx<_Filter> _filter = Rx(_Filter.all);
|
||||
final Rx<_Sort> _sort = Rx(_Sort.timeDescending);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
final data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry.id != _hostingController.uuid) && entry.discoverable)
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<FortniteServer> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildFilter(context),
|
||||
const SizedBox(
|
||||
width: 16.0
|
||||
),
|
||||
_buildSort(context),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Expanded(
|
||||
child: _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildSort(BuildContext context) => Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.arrow_sort_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Sort by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 230,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_sort.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Sort.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _sort.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
|
||||
Row _buildFilter(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
fluentUiIcons.FluentIcons.filter_24_regular,
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Text(
|
||||
"Filter by: ",
|
||||
style: TextStyle(
|
||||
color: FluentTheme.of(context).resources.textFillColorDisabled
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4.0),
|
||||
Obx(() => SizedBox(
|
||||
width: 125,
|
||||
child: DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(
|
||||
_filter.value.translatedName,
|
||||
textAlign: TextAlign.start
|
||||
),
|
||||
title: const Spacer(),
|
||||
items: _Filter.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.translatedName),
|
||||
onPressed: () => _filter.value = entry
|
||||
)).toList()
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopulatedListBody(Set<FortniteServer> items) => Obx(() {
|
||||
final filter = _filter.value;
|
||||
final sorted = items.where((element) {
|
||||
switch(filter) {
|
||||
case _Filter.all:
|
||||
return true;
|
||||
case _Filter.accessible:
|
||||
return element.password == null;
|
||||
case _Filter.playable:
|
||||
return _gameController.getVersionByName(element.version) != null;
|
||||
}
|
||||
}).toList();
|
||||
final sort = _sort.value;
|
||||
sorted.sort((first, second) {
|
||||
switch(sort) {
|
||||
case _Sort.timeAscending:
|
||||
return first.timestamp.compareTo(second.timestamp);
|
||||
case _Sort.timeDescending:
|
||||
return second.timestamp.compareTo(first.timestamp);
|
||||
case _Sort.nameAscending:
|
||||
return first.name.compareTo(second.name);
|
||||
case _Sort.nameDescending:
|
||||
return second.name.compareTo(first.name);
|
||||
}
|
||||
});
|
||||
if(sorted.isEmpty) {
|
||||
return _noServersByQuery;
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = sorted.elementAt(index);
|
||||
final hasPassword = entry.password != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry.author}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServerInteractive(_hostingController.uuid, entry),
|
||||
child: Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(FortniteServer entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(FortniteServer element, String filter) {
|
||||
filter = filter.toLowerCase();
|
||||
|
||||
final uri = Uri.tryParse(filter);
|
||||
if(uri != null && uri.host.isNotEmpty && element.id.toLowerCase().contains(uri.host.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return element.id.toLowerCase().contains(filter.toLowerCase())
|
||||
|| element.name.toLowerCase().contains(filter)
|
||||
|| element.author.toLowerCase().contains(filter)
|
||||
|| element.description.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
final color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(FortniteServer server) {
|
||||
final result = server.name;
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(FortniteServer server) {
|
||||
final result = server.description;
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(FortniteServer server) => "Fortnite ${server.version.toString()}";
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
|
||||
enum _Filter {
|
||||
all,
|
||||
accessible,
|
||||
playable;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Filter.all:
|
||||
return translations.all;
|
||||
case _Filter.accessible:
|
||||
return translations.accessible;
|
||||
case _Filter.playable:
|
||||
return translations.playable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _Sort {
|
||||
timeAscending,
|
||||
timeDescending,
|
||||
nameAscending,
|
||||
nameDescending;
|
||||
|
||||
String get translatedName {
|
||||
switch(this) {
|
||||
case _Sort.timeAscending:
|
||||
return translations.timeAscending;
|
||||
case _Sort.timeDescending:
|
||||
return translations.timeDescending;
|
||||
case _Sort.nameAscending:
|
||||
return translations.nameAscending;
|
||||
case _Sort.nameDescending:
|
||||
return translations.nameDescending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,14 @@ import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_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/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.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/dll.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';
|
||||
@@ -29,9 +28,12 @@ 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';
|
||||
|
||||
import 'info_page.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
|
||||
@@ -39,16 +41,14 @@ class HomePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
|
||||
static const double _kDefaultPadding = 12.0;
|
||||
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
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;
|
||||
@@ -56,7 +56,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowManager.setPreventClose(true);
|
||||
windowManager.addListener(this);
|
||||
_syncPageViewWithNavigator();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkUpdates();
|
||||
_initAppLink();
|
||||
@@ -64,6 +66,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -78,9 +92,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
final uuid = uri.host;
|
||||
final server = _hostingController.findServerById(uuid);
|
||||
if(server != null) {
|
||||
_backendController.joinServer(_hostingController.uuid, server);
|
||||
_backendController.joinServerInteractive(_hostingController.uuid, server);
|
||||
}else {
|
||||
showInfoBar(
|
||||
showRebootInfoBar(
|
||||
translations.noServerFound,
|
||||
duration: infoBarLongDuration,
|
||||
severity: InfoBarSeverity.error
|
||||
@@ -100,10 +114,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
return;
|
||||
}
|
||||
|
||||
var oldOwner = _backendController.gameServerOwner.value;
|
||||
_backendController.joinLocalHost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
|
||||
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
|
||||
_backendController.joinLocalhost();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar(
|
||||
translations.serverNoLongerAvailableUnnamed,
|
||||
severity: InfoBarSeverity.warning,
|
||||
duration: infoBarLongDuration
|
||||
));
|
||||
@@ -114,27 +127,34 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
}
|
||||
|
||||
void _checkUpdates() {
|
||||
_updateController.notifyLauncherUpdate();
|
||||
_settingsController.notifyLauncherUpdate();
|
||||
|
||||
if(!dllsDirectory.existsSync()) {
|
||||
dllsDirectory.createSync(recursive: true);
|
||||
}
|
||||
|
||||
for(final injectable in InjectableDll.values) {
|
||||
downloadCriticalDllInteractive(
|
||||
injectable.path,
|
||||
silent: true
|
||||
);
|
||||
final (file, custom) = _settingsController.getInjectableData(injectable);
|
||||
if(!custom) {
|
||||
_settingsController.downloadCriticalDllInteractive(
|
||||
file.path,
|
||||
silent: true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
watchDlls().listen((filePath) => showDllDeletedDialog(() {
|
||||
downloadCriticalDllInteractive(filePath);
|
||||
_settingsController.downloadCriticalDllInteractive(filePath);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
exit(0); // Force closing
|
||||
void onWindowClose() async {
|
||||
try {
|
||||
await _hostingController.discardServer();
|
||||
}finally {
|
||||
exit(0); // Force closing
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -153,7 +173,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
|
||||
@override
|
||||
void onWindowBlur() {
|
||||
_focused.value = false;
|
||||
_focused.value = !_focused.value;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -218,137 +238,364 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
_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);
|
||||
// InfoPage.initInfoTiles();
|
||||
return Obx(() {
|
||||
return NavigationPaneTheme(
|
||||
data: NavigationPaneThemeData(
|
||||
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
|
||||
),
|
||||
child: NavigationView(
|
||||
paneBodyBuilder: (pane, body) => _PaneBody(
|
||||
padding: _kDefaultPadding,
|
||||
controller: pagesController,
|
||||
body: body
|
||||
),
|
||||
appBar: NavigationAppBar(
|
||||
height: 32,
|
||||
title: _draggableArea,
|
||||
actions: WindowTitleBar(focused: _focused()),
|
||||
leading: _backButton,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
pane: NavigationPane(
|
||||
selected: pageIndex.value,
|
||||
onChanged: (index) {
|
||||
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);
|
||||
}
|
||||
},
|
||||
menuButton: const SizedBox(),
|
||||
displayMode: PaneDisplayMode.open,
|
||||
items: _items,
|
||||
customPane: _CustomPane(_settingsController),
|
||||
header: const ProfileWidget(),
|
||||
autoSuggestBox: _autoSuggestBox,
|
||||
indicator: const StickyNavigationIndicator(
|
||||
duration: Duration(milliseconds: 500),
|
||||
curve: Curves.easeOut,
|
||||
indicatorSize: 3.25
|
||||
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()
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
contentShape: const RoundedRectangleBorder(),
|
||||
onOpenSearch: () => _searchFocusNode.requestFocus(),
|
||||
transitionBuilder: (child, animation) => child
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
return 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.only(top: 6.0)),
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
onPressed: appStack.isEmpty && !inDialog ? null : () {
|
||||
if(inDialog) {
|
||||
Navigator.of(appKey.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();
|
||||
}
|
||||
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),
|
||||
)
|
||||
}
|
||||
pagesController.add(null);
|
||||
},
|
||||
child: const Icon(FluentIcons.back, size: 12.0),
|
||||
)
|
||||
);
|
||||
|
||||
GestureDetector get _draggableArea => GestureDetector(
|
||||
onDoubleTap: appWindow.maximizeOrRestore,
|
||||
onHorizontalDragStart: (_) => appWindow.startDragging(),
|
||||
onVerticalDragStart: (_) => appWindow.startDragging()
|
||||
onHorizontalDragStart: (_) => windowManager.startDragging(),
|
||||
onVerticalDragStart: (_) => windowManager.startDragging()
|
||||
);
|
||||
|
||||
Widget get _autoSuggestBox => Obx(() {
|
||||
final firstRun = _settingsController.firstRun.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0
|
||||
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
|
||||
)
|
||||
),
|
||||
child: AutoSuggestBox<PageSuggestion>(
|
||||
key: _searchKey,
|
||||
controller: _searchController,
|
||||
enabled: !firstRun,
|
||||
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)
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
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(
|
||||
@@ -367,282 +614,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
|
||||
));
|
||||
return results;
|
||||
}).toList();
|
||||
|
||||
List<NavigationPaneItem> get _items => pages.map((page) => _createItem(page)).toList();
|
||||
|
||||
NavigationPaneItem _createItem(RebootPage page) => PaneItem(
|
||||
title: Text(page.name),
|
||||
icon: SizedBox.square(
|
||||
dimension: 24,
|
||||
child: Image.asset(page.iconAsset)
|
||||
),
|
||||
body: page
|
||||
);
|
||||
}
|
||||
|
||||
class _PaneBody extends StatefulWidget {
|
||||
const _PaneBody({
|
||||
required this.padding,
|
||||
required this.controller,
|
||||
required this.body
|
||||
});
|
||||
|
||||
final double padding;
|
||||
final StreamController<void> controller;
|
||||
final Widget? body;
|
||||
|
||||
@override
|
||||
State<_PaneBody> createState() => _PaneBodyState();
|
||||
}
|
||||
|
||||
class _PaneBodyState extends State<_PaneBody> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final PageController _pageController = PageController(keepPage: true, initialPage: pageIndex.value);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
var lastPage = pageIndex.value;
|
||||
pageIndex.listen((index) {
|
||||
if(index == lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPage = index;
|
||||
_pageController.jumpToPage(index);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final themeMode = _settingsController.themeMode.value;
|
||||
final inactiveColor = themeMode == ThemeMode.dark
|
||||
|| (themeMode == ThemeMode.system && isDarkMode) ? Colors.grey[60] : Colors.grey[100];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.padding,
|
||||
right: widget.padding * 2,
|
||||
top: widget.padding,
|
||||
bottom: widget.padding * 2
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 1000
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: StreamBuilder(
|
||||
stream: widget.controller.stream,
|
||||
builder: (context, _) {
|
||||
final elements = <TextSpan>[];
|
||||
elements.add(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);
|
||||
}
|
||||
|
||||
widget.controller.add(null);
|
||||
}) : null,
|
||||
style: TextStyle(
|
||||
color: pageStack.isNotEmpty ? inactiveColor : null
|
||||
)
|
||||
));
|
||||
for(var i = pageStack.length - 1; i >= 0; i--) {
|
||||
var innerPage = pageStack.elementAt(i);
|
||||
innerPage = innerPage.substring(innerPage.indexOf("_") + 1);
|
||||
elements.add(TextSpan(
|
||||
text: " > ",
|
||||
style: TextStyle(
|
||||
color: inactiveColor
|
||||
)
|
||||
));
|
||||
elements.add(TextSpan(
|
||||
text: innerPage,
|
||||
recognizer: i == pageStack.length - 1 ? null : (TapGestureRecognizer()..onTap = () {
|
||||
if(inDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var j = 0; j < i - 1; j++) {
|
||||
Navigator.of(pageKey.currentContext!).pop();
|
||||
final element = pageStack.removeLast();
|
||||
appStack.remove(element);
|
||||
}
|
||||
widget.controller.add(null);
|
||||
}),
|
||||
style: TextStyle(
|
||||
color: i == pageStack.length - 1 ? null : inactiveColor
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: elements
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24.0),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.loose,
|
||||
children: [
|
||||
PageView.builder(
|
||||
controller: _pageController,
|
||||
itemBuilder: (context, index) => Navigator(
|
||||
onPopPage: (page, data) => true,
|
||||
observers: [
|
||||
_NestedPageObserver(
|
||||
onChanged: (routeName) {
|
||||
if(routeName != null) {
|
||||
pageIndex.refresh();
|
||||
addSubPageToStack(routeName);
|
||||
widget.controller.add(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: KeyedSubtree(
|
||||
key: getPageKeyByIndex(index),
|
||||
child: widget.body ?? const SizedBox.shrink()
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
itemCount: pages.length
|
||||
),
|
||||
InfoBarArea(
|
||||
key: infoBarAreaKey
|
||||
)
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomPane extends NavigationPaneWidget {
|
||||
final SettingsController settingsController;
|
||||
_CustomPane(this.settingsController);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
data.appBar,
|
||||
Expanded(
|
||||
child: Navigator(
|
||||
key: appKey,
|
||||
onPopPage: (page, data) => false,
|
||||
pages: [
|
||||
MaterialPage(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 310,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
data.pane.header ?? const SizedBox.shrink(),
|
||||
data.pane.autoSuggestBox ?? const SizedBox.shrink(),
|
||||
const SizedBox(height: 12.0),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: data.scrollController,
|
||||
child: ListView.separated(
|
||||
controller: data.scrollController,
|
||||
itemCount: data.pane.items.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 4.0
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = data.pane.items[index] as PaneItem;
|
||||
return Obx(() {
|
||||
final firstRun = settingsController.firstRun.value;
|
||||
return HoverButton(
|
||||
onPressed: firstRun ? null : () => data.pane.onChanged?.call(index),
|
||||
builder: (context, states) => Container(
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
item == data.pane.selectedItem ? {ButtonStates.hovering} : states,
|
||||
transparentWhenNone: true,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
data.pane.indicator ?? const SizedBox.shrink(),
|
||||
item.icon,
|
||||
const SizedBox(width: 12.0),
|
||||
item.title ?? const SizedBox.shrink()
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: data.content
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NestedPageObserver extends NavigatorObserver {
|
||||
|
||||
@@ -10,11 +10,10 @@ import 'package:reboot_launcher/main.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_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/controller/update_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.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/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -23,7 +22,13 @@ import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
import '../../util/checks.dart';
|
||||
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoNameOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoDescriptionOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostInfoPasswordOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<OverlayTargetState> hostShareOverlayTargetKey = GlobalKey();
|
||||
final GlobalKey<SettingTileState> hostInfoTileKey = GlobalKey();
|
||||
|
||||
class HostPage extends RebootPage {
|
||||
const HostPage({Key? key}) : super(key: key);
|
||||
@@ -47,7 +52,6 @@ class HostPage extends RebootPage {
|
||||
class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final UpdateController _updateController = Get.find<UpdateController>();
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
|
||||
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
|
||||
@@ -67,39 +71,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
|
||||
@override
|
||||
Widget get button => LaunchButton(
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
host: true,
|
||||
startLabel: translations.startHosting,
|
||||
stopLabel: translations.stopHosting
|
||||
);
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
_information,
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: hostVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_share,
|
||||
_resetDefaults
|
||||
];
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _information => SettingTile(
|
||||
key: hostInfoTileKey,
|
||||
icon: Icon(
|
||||
FluentIcons.info_24_regular
|
||||
),
|
||||
title: Text(translations.hostGameServerName),
|
||||
subtitle: Text(translations.hostGameServerDescription),
|
||||
overlayKey: hostInfoOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -107,10 +104,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerNameName),
|
||||
subtitle: Text(translations.hostGameServerNameDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoNameOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerNameName,
|
||||
controller: _hostingController.name,
|
||||
focusNode: _hostingController.nameFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -119,10 +120,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerDescriptionName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
onChanged: (_) => _updateServer()
|
||||
content: OverlayTarget(
|
||||
key: hostInfoDescriptionOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerDescriptionName,
|
||||
controller: _hostingController.description,
|
||||
focusNode: _hostingController.descriptionFocusNode,
|
||||
onChanged: (_) => _updateServer()
|
||||
),
|
||||
)
|
||||
),
|
||||
SettingTile(
|
||||
@@ -131,28 +136,32 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
),
|
||||
title: Text(translations.hostGameServerPasswordName),
|
||||
subtitle: Text(translations.hostGameServerDescriptionDescription),
|
||||
content: Obx(() => TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
content: Obx(() => OverlayTarget(
|
||||
key: hostInfoPasswordOverlayTargetKey,
|
||||
child: TextFormBox(
|
||||
placeholder: translations.hostGameServerPasswordName,
|
||||
controller: _hostingController.password,
|
||||
focusNode: _hostingController.passwordFocusNode,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
obscureText: !_hostingController.showPassword.value,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
onChanged: (text) {
|
||||
_showPasswordTrailing.value = text.isNotEmpty;
|
||||
_updateServer();
|
||||
},
|
||||
suffix: Button(
|
||||
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
|
||||
style: ButtonStyle(
|
||||
shape: ButtonState.all(const CircleBorder()),
|
||||
backgroundColor: ButtonState.all(Colors.transparent)
|
||||
),
|
||||
child: Icon(
|
||||
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
|
||||
color: _showPasswordTrailing.value ? null : Colors.transparent
|
||||
),
|
||||
)
|
||||
),
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
@@ -183,105 +192,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _updateController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateController.customGameServer.value = entry.key;
|
||||
_updateController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _updateController.url,
|
||||
validator: checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_updateController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_updateController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_updateController.timer.value = entry;
|
||||
_updateController.infoBarEntry?.close();
|
||||
_updateController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _options => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
@@ -347,12 +257,121 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerName),
|
||||
subtitle: Text(translations.settingsServerSubtitle),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTypeName),
|
||||
subtitle: Text(translations.settingsServerTypeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
|
||||
items: {
|
||||
false: translations.settingsServerTypeEmbeddedName,
|
||||
true: translations.settingsServerTypeCustomName
|
||||
}.entries.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.value),
|
||||
onPressed: () {
|
||||
final oldValue = _settingsController.customGameServer.value;
|
||||
if(oldValue == entry.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
_settingsController.customGameServer.value = entry.key;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
if(!entry.key) {
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
Obx(() {
|
||||
if(!_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return createFileSetting(
|
||||
title: translations.settingsServerFileName,
|
||||
description: translations.settingsServerFileDescription,
|
||||
controller: _settingsController.gameServerDll
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerMirrorName),
|
||||
subtitle: Text(translations.settingsServerMirrorDescription),
|
||||
content: TextFormBox(
|
||||
placeholder: translations.settingsServerMirrorPlaceholder,
|
||||
controller: _settingsController.url,
|
||||
validator: _checkUpdateUrl
|
||||
)
|
||||
);
|
||||
}),
|
||||
Obx(() {
|
||||
if(_settingsController.customGameServer.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.timer_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerTimerName),
|
||||
subtitle: Text(translations.settingsServerTimerSubtitle),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.timer.value.text),
|
||||
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
|
||||
text: Text(entry.text),
|
||||
onPressed: () {
|
||||
_settingsController.timer.value = entry;
|
||||
_settingsController.infoBarEntry?.close();
|
||||
_settingsController.updateReboot(
|
||||
force: true
|
||||
);
|
||||
}
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
String? _checkUpdateUrl(String? text) {
|
||||
if (text == null || text.isEmpty) {
|
||||
return translations.emptyURL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
SettingTile get _share => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.link_24_regular
|
||||
),
|
||||
title: Text(translations.hostShareName),
|
||||
subtitle: Text(translations.hostShareDescription),
|
||||
overlayKey: hostShareOverlayTargetKey,
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -382,7 +401,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
final ip = await Ipify.ipv4();
|
||||
entry.close();
|
||||
FlutterClipboard.controlC(ip);
|
||||
_showCopiedIp();
|
||||
_showCopiedIp();
|
||||
}catch(error) {
|
||||
entry?.close();
|
||||
_showCannotCopyIp(error);
|
||||
@@ -394,6 +413,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.hostResetName),
|
||||
subtitle: Text(translations.hostResetDescription),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_hostingController.reset),
|
||||
child: Text(translations.hostResetContent),
|
||||
)
|
||||
);
|
||||
|
||||
Future<void> _updateServer() async {
|
||||
if(!_hostingController.published()) {
|
||||
return;
|
||||
@@ -409,29 +440,29 @@ class _HostingPageState extends RebootPageState<HostPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showCopiedLink() => showInfoBar(
|
||||
void _showCopiedLink() => showRebootInfoBar(
|
||||
translations.hostShareLinkMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
InfoBarEntry _showCopyingIp() => showInfoBar(
|
||||
InfoBarEntry _showCopyingIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageLoading,
|
||||
loading: true,
|
||||
duration: null
|
||||
);
|
||||
|
||||
void _showCopiedIp() => showInfoBar(
|
||||
void _showCopiedIp() => showRebootInfoBar(
|
||||
translations.hostShareIpMessageSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
|
||||
void _showCannotCopyIp(Object error) => showInfoBar(
|
||||
void _showCannotCopyIp(Object error) => showRebootInfoBar(
|
||||
translations.hostShareIpMessageError(error.toString()),
|
||||
severity: InfoBarSeverity.error,
|
||||
duration: infoBarLongDuration
|
||||
);
|
||||
|
||||
void _showCannotUpdateGameServer(Object error) => showInfoBar(
|
||||
void _showCannotUpdateGameServer(Object error) => showRebootInfoBar(
|
||||
translations.cannotUpdateGameServer(error.toString()),
|
||||
severity: InfoBarSeverity.success,
|
||||
duration: infoBarLongDuration
|
||||
@@ -1,80 +1,14 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_launcher/src/widget/info_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class InfoPage extends RebootPage {
|
||||
static late List<InfoTile> _infoTiles;
|
||||
static late List<_QuizEntry> _quizEntries;
|
||||
|
||||
static Object? initInfoTiles() {
|
||||
try {
|
||||
final faqDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\faq");
|
||||
final infoTiles = SplayTreeMap<int, InfoTile>();
|
||||
for(final entry in faqDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
infoTiles[index] = InfoTile(
|
||||
title: Text(questionName),
|
||||
content: Text(entry.readAsStringSync())
|
||||
);
|
||||
}
|
||||
}
|
||||
_infoTiles = infoTiles.values.toList(growable: false);
|
||||
|
||||
final questionsDirectory = Directory("${assetsDirectory.path}\\info\\$currentLocale\\questions");
|
||||
final questions = SplayTreeMap<int, _QuizEntry>();
|
||||
for(final entry in questionsDirectory.listSync()) {
|
||||
if(entry is File) {
|
||||
final name = Uri.decodeQueryComponent(path.basename(entry.path));
|
||||
final splitter = name.indexOf(".");
|
||||
if(splitter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final index = int.tryParse(name.substring(0, splitter));
|
||||
if(index == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
|
||||
questions[index] = _QuizEntry(
|
||||
question: questionName,
|
||||
options: entry.readAsStringSync().split("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
_quizEntries = questions.values.toList(growable: false);
|
||||
|
||||
return null;
|
||||
}catch(error) {
|
||||
_infoTiles = [];
|
||||
_quizEntries = [];
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
const InfoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -87,207 +21,59 @@ class InfoPage extends RebootPage {
|
||||
String get iconAsset => "assets/images/info.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => Get.find<SettingsController>().firstRun.value && pageName != null;
|
||||
bool hasButton(String? routeName) => false;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.info;
|
||||
}
|
||||
|
||||
class _InfoPageState extends RebootPageState<InfoPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final Rxn<Widget> _quizPage;
|
||||
static const String _kReportBugUrl = "https://github.com/Auties00/reboot_launcher/issues/new";
|
||||
static const String _kDiscordInviteUrl = "https://discord.gg/reboot";
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
_discord,
|
||||
_tutorial,
|
||||
_reportBug
|
||||
];
|
||||
|
||||
SettingTile get _reportBug => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.bug_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsBugReportName),
|
||||
subtitle: Text(translations.settingsUtilsBugReportSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kReportBugUrl),
|
||||
child: Text(translations.settingsUtilsBugReportContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _tutorial => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.chat_help_24_regular
|
||||
),
|
||||
title: Text(translations.infoVideoName),
|
||||
subtitle: Text(translations.infoVideoDescription),
|
||||
content: Button(
|
||||
onPressed: () => startOnboarding(),
|
||||
child: Text(translations.infoVideoContent)
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _discord => SettingTile(
|
||||
icon: Icon(
|
||||
Icons.discord_outlined
|
||||
),
|
||||
title: Text(translations.infoDiscordName),
|
||||
subtitle: Text(translations.infoDiscordDescription),
|
||||
content: Button(
|
||||
onPressed: () => launchUrlString(_kDiscordInviteUrl),
|
||||
child: Text(translations.infoDiscordContent)
|
||||
)
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_quizPage = Rxn(_settingsController.firstRun.value ? _QuizRoute(
|
||||
entries: InfoPage._quizEntries,
|
||||
onSuccess: () => _quizPage.value = null
|
||||
) : null);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Widget> get settings => InfoPage._infoTiles;
|
||||
|
||||
@override
|
||||
Widget? get button {
|
||||
if(_quizPage.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final page = _quizPage.value;
|
||||
if(page == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Button(
|
||||
onPressed: () => Navigator.of(context).push(PageRouteBuilder(
|
||||
transitionDuration: Duration.zero,
|
||||
reverseTransitionDuration: Duration.zero,
|
||||
settings: RouteSettings(
|
||||
name: translations.quiz
|
||||
),
|
||||
pageBuilder: (context, incoming, outgoing) => page
|
||||
)),
|
||||
child: Text(
|
||||
translations.startQuiz
|
||||
),
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizRoute extends StatefulWidget {
|
||||
final List<_QuizEntry> entries;
|
||||
final void Function() onSuccess;
|
||||
const _QuizRoute({
|
||||
required this.entries,
|
||||
required this.onSuccess
|
||||
});
|
||||
|
||||
@override
|
||||
State<_QuizRoute> createState() => _QuizRouteState();
|
||||
}
|
||||
|
||||
class _QuizRouteState extends State<_QuizRoute> with AutomaticKeepAliveClientMixin {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
late final List<RxInt> _selectedIndexes = List.generate(widget.entries.length, (_) => RxInt(-1));
|
||||
int _triesLeft = 3;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: widget.entries.indexed.expand((entry) {
|
||||
final selectedIndex = _selectedIndexes[entry.$1];
|
||||
return [
|
||||
Text(
|
||||
"${entry.$1 + 1}. ${entry.$2.question}",
|
||||
style: TextStyle(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w600
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0),
|
||||
...entry.$2.options.indexed.map<Widget>((value) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Obx(() => RadioButton(
|
||||
checked: value.$1 == selectedIndex.value,
|
||||
content: Text(value.$2, textAlign: TextAlign.center),
|
||||
onChanged: (_) => selectedIndex.value = value.$1
|
||||
)),
|
||||
)),
|
||||
const SizedBox(height: 12.0)
|
||||
];
|
||||
}).toList()
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: Obx(() {
|
||||
var clickable = true;
|
||||
for(final index in _selectedIndexes) {
|
||||
if(index.value == -1) {
|
||||
clickable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Button(
|
||||
onPressed: clickable ? () async {
|
||||
if(_triesLeft <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var right = 0;
|
||||
final total = widget.entries.length;
|
||||
for(var i = 0; i < total; i++) {
|
||||
final selectedIndex = _selectedIndexes[i].value;
|
||||
final correctIndex = widget.entries[i].correctIndex;
|
||||
if(selectedIndex == correctIndex) {
|
||||
right++;
|
||||
}
|
||||
}
|
||||
|
||||
if(right == total) {
|
||||
widget.onSuccess();
|
||||
showInfoBar(
|
||||
translations.quizSuccess,
|
||||
severity: InfoBarSeverity.success
|
||||
);
|
||||
_settingsController.firstRun.value = false;
|
||||
Navigator.of(context).pop();
|
||||
pageIndex.value = RebootPageType.play.index;
|
||||
return;
|
||||
}
|
||||
|
||||
switch(--_triesLeft) {
|
||||
case 0:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizZeroTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
exit(0);
|
||||
case 1:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizOneTryLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
showInfoBar(
|
||||
translations.quizFailed(
|
||||
right,
|
||||
total,
|
||||
translations.quizTwoTriesLeft
|
||||
),
|
||||
severity: InfoBarSeverity.error
|
||||
);
|
||||
break;
|
||||
}
|
||||
} : null,
|
||||
child: Text(translations.checkQuiz),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuizEntry {
|
||||
final String question;
|
||||
final List<String> options;
|
||||
late final int correctIndex;
|
||||
|
||||
_QuizEntry({required this.question, required this.options}) {
|
||||
final correct = options.first;
|
||||
options.shuffle();
|
||||
correctIndex = options.indexOf(correct);
|
||||
}
|
||||
Widget? get button => null;
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_launcher/src/controller/backend_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/page/pages.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/game_start_button.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
|
||||
|
||||
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
|
||||
|
||||
class PlayPage extends RebootPage {
|
||||
const PlayPage({Key? key}) : super(key: key);
|
||||
@@ -36,8 +37,49 @@ class PlayPage extends RebootPage {
|
||||
class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||
final GameController _gameController = Get.find<GameController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildFirstLaunchInfo(),
|
||||
Expanded(
|
||||
child: super.build(context),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFirstLaunchInfo() => Obx(() {
|
||||
if(!_settingsController.firstRun.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 8.0
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: InfoBar(
|
||||
title: Text(translations.welcomeTitle),
|
||||
severity: InfoBarSeverity.warning,
|
||||
isLong: true,
|
||||
content: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(translations.welcomeDescription)
|
||||
),
|
||||
action: Button(
|
||||
child: Text(translations.welcomeAction),
|
||||
onPressed: () => startOnboarding(),
|
||||
),
|
||||
onClose: () => _settingsController.firstRun.value = false
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Widget? get button => LaunchButton(
|
||||
startLabel: translations.launchFortnite,
|
||||
@@ -47,25 +89,13 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
|
||||
@override
|
||||
List<SettingTile> get settings => [
|
||||
versionSelectSettingTile,
|
||||
buildVersionSelector(
|
||||
key: gameVersionOverlayTargetKey
|
||||
),
|
||||
_options,
|
||||
_internalFiles,
|
||||
_multiplayer
|
||||
];
|
||||
|
||||
SettingTile get _multiplayer => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.people_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerName),
|
||||
subtitle: Text(translations.playGameServerDescription),
|
||||
children: [
|
||||
_hostSettingTile,
|
||||
_browseServerTile,
|
||||
_matchmakerTile,
|
||||
],
|
||||
);
|
||||
|
||||
SettingTile get _internalFiles => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.archive_settings_24_regular
|
||||
@@ -95,8 +125,8 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
icon: Icon(
|
||||
FluentIcons.options_24_regular
|
||||
),
|
||||
title: Text(translations.settingsServerOptionsName),
|
||||
subtitle: Text(translations.settingsServerOptionsSubtitle),
|
||||
title: Text(translations.settingsClientOptionsName),
|
||||
subtitle: Text(translations.settingsClientOptionsDescription),
|
||||
children: [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
@@ -111,34 +141,4 @@ class _PlayPageState extends RebootPageState<PlayPage> {
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
SettingTile get _matchmakerTile => SettingTile(
|
||||
onPressed: () {
|
||||
pageIndex.value = RebootPageType.backend.index;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _backendController.gameServerAddressFocusNode.requestFocus());
|
||||
},
|
||||
icon: Icon(
|
||||
FluentIcons.globe_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerCustomName),
|
||||
subtitle: Text(translations.playGameServerCustomDescription),
|
||||
);
|
||||
|
||||
SettingTile get _browseServerTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.browser.index,
|
||||
icon: Icon(
|
||||
FluentIcons.search_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerBrowserName),
|
||||
subtitle: Text(translations.playGameServerBrowserDescription)
|
||||
);
|
||||
|
||||
SettingTile get _hostSettingTile => SettingTile(
|
||||
onPressed: () => pageIndex.value = RebootPageType.host.index,
|
||||
icon: Icon(
|
||||
FluentIcons.desktop_24_regular
|
||||
),
|
||||
title: Text(translations.playGameServerHostName),
|
||||
subtitle: Text(translations.playGameServerHostDescription),
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/hosting_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
import 'package:reboot_launcher/src/widget/setting_tile.dart';
|
||||
|
||||
class BrowsePage extends RebootPage {
|
||||
const BrowsePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
String get name => translations.browserName;
|
||||
|
||||
@override
|
||||
RebootPageType get type => RebootPageType.browser;
|
||||
|
||||
@override
|
||||
String get iconAsset => "assets/images/server_browser.png";
|
||||
|
||||
@override
|
||||
bool hasButton(String? pageName) => false;
|
||||
|
||||
@override
|
||||
RebootPageState<BrowsePage> createState() => _BrowsePageState();
|
||||
}
|
||||
|
||||
class _BrowsePageState extends RebootPageState<BrowsePage> {
|
||||
final HostingController _hostingController = Get.find<HostingController>();
|
||||
final BackendController _backendController = Get.find<BackendController>();
|
||||
final TextEditingController _filterController = TextEditingController();
|
||||
final StreamController<String> _filterControllerStream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Obx(() {
|
||||
var data = _hostingController.servers.value
|
||||
?.where((entry) => (kDebugMode || entry["id"] != _hostingController.uuid) && entry["discoverable"])
|
||||
.toSet();
|
||||
if(data == null || data.isEmpty == true) {
|
||||
return _noServers;
|
||||
}
|
||||
|
||||
return _buildPageBody(data);
|
||||
});
|
||||
}
|
||||
|
||||
Widget get _noServers => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableSubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildPageBody(Set<Map<String, dynamic>> data) => StreamBuilder(
|
||||
stream: _filterControllerStream.stream,
|
||||
builder: (context, filterSnapshot) {
|
||||
final items = data.where((entry) => _isValidItem(entry, filterSnapshot.data)).toSet();
|
||||
return Column(
|
||||
children: [
|
||||
_searchBar,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: items.isEmpty ? _noServersByQuery : _buildPopulatedListBody(items)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget _buildPopulatedListBody(Set<Map<String, dynamic>> items) => ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var entry = items.elementAt(index ~/ 2);
|
||||
var hasPassword = entry["password"] != null;
|
||||
return SettingTile(
|
||||
icon: Icon(
|
||||
hasPassword ? FluentIcons.lock : FluentIcons.globe
|
||||
),
|
||||
title: Text("${_formatName(entry)} • ${entry["author"]}"),
|
||||
subtitle: Text("${_formatDescription(entry)} • ${_formatVersion(entry)}"),
|
||||
content: Button(
|
||||
onPressed: () => _backendController.joinServer(_hostingController.uuid, entry),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(_backendController.type.value == ServerType.embedded ? translations.joinServer : translations.copyIp),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Widget get _noServersByQuery => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
translations.noServersAvailableByQueryTitle,
|
||||
style: FluentTheme.of(context).typography.titleLarge,
|
||||
),
|
||||
Text(
|
||||
translations.noServersAvailableByQuerySubtitle,
|
||||
style: FluentTheme.of(context).typography.body
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
bool _isValidItem(Map<String, dynamic> entry, String? filter) =>
|
||||
filter == null || filter.isEmpty || _filterServer(entry, filter);
|
||||
|
||||
bool _filterServer(Map<String, dynamic> element, String filter) {
|
||||
String? id = element["id"];
|
||||
if(id?.toLowerCase().contains(filter.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var uri = Uri.tryParse(filter);
|
||||
if(uri != null
|
||||
&& uri.host.isNotEmpty
|
||||
&& id?.toLowerCase().contains(uri.host.toLowerCase()) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? name = element["name"];
|
||||
if(name?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? author = element["author"];
|
||||
if(author?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String? description = element["description"];
|
||||
if(description?.toLowerCase().contains(filter) == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Widget get _searchBar => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 350
|
||||
),
|
||||
child: TextBox(
|
||||
placeholder: translations.findServer,
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
onChanged: (value) => _filterControllerStream.add(value),
|
||||
suffix: _searchBarIcon,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget get _searchBarIcon => Button(
|
||||
onPressed: _filterController.text.isEmpty ? null : () {
|
||||
_filterController.clear();
|
||||
_filterControllerStream.add("");
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
shape: ButtonState.all(Border())
|
||||
),
|
||||
child: _searchBarIconData
|
||||
);
|
||||
|
||||
Widget get _searchBarIconData {
|
||||
var color = FluentTheme.of(context).resources.textFillColorPrimary;
|
||||
if (_filterController.text.isNotEmpty) {
|
||||
return Icon(
|
||||
FluentIcons.clear,
|
||||
size: 8.0,
|
||||
color: color
|
||||
);
|
||||
}
|
||||
|
||||
return Transform.flip(
|
||||
flipX: true,
|
||||
child: Icon(
|
||||
FluentIcons.search,
|
||||
size: 12.0,
|
||||
color: color
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatName(Map<String, dynamic> entry) {
|
||||
String result = entry['name'];
|
||||
return result.isEmpty ? translations.defaultServerName : result;
|
||||
}
|
||||
|
||||
String _formatDescription(Map<String, dynamic> entry) {
|
||||
String result = entry['description'];
|
||||
return result.isEmpty ? translations.defaultServerDescription : result;
|
||||
}
|
||||
|
||||
String _formatVersion(Map<String, dynamic> entry) {
|
||||
var version = entry['version'];
|
||||
var versionSplit = version.indexOf("-");
|
||||
var minimalVersion = version = versionSplit != -1 ? version.substring(0, versionSplit) : version;
|
||||
String result = minimalVersion.endsWith(".0") ? minimalVersion.substring(0, minimalVersion.length - 2) : minimalVersion;
|
||||
if(result.toLowerCase().startsWith("fortnite ")) {
|
||||
result = result.substring(0, 10);
|
||||
}
|
||||
|
||||
return "Fortnite $result";
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? get button => null;
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [];
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/dialog/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
|
||||
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page.dart';
|
||||
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
|
||||
import 'package:reboot_launcher/src/util/translations.dart';
|
||||
@@ -40,52 +40,67 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
|
||||
@override
|
||||
List<Widget> get settings => [
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
),
|
||||
SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
),
|
||||
_language,
|
||||
_theme,
|
||||
_resetDefaults,
|
||||
_installationDirectory
|
||||
];
|
||||
|
||||
SettingTile get _language => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.local_language_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsLanguageName),
|
||||
subtitle: Text(translations.settingsUtilsLanguageDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_getLocaleName(_settingsController.language.value)),
|
||||
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
|
||||
text: Text(_getLocaleName(locale.languageCode)),
|
||||
onPressed: () => _settingsController.language.value = locale.languageCode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
SettingTile get _theme => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.dark_theme_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsThemeName),
|
||||
subtitle: Text(translations.settingsUtilsThemeDescription),
|
||||
content: Obx(() => DropDownButton(
|
||||
onOpen: () => inDialog = true,
|
||||
onClose: () => inDialog = false,
|
||||
leading: Text(_settingsController.themeMode.value.title),
|
||||
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
|
||||
text: Text(themeMode.title),
|
||||
onPressed: () => _settingsController.themeMode.value = themeMode
|
||||
)).toList()
|
||||
))
|
||||
);
|
||||
|
||||
SettingTile get _resetDefaults => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.arrow_reset_24_regular
|
||||
),
|
||||
title: Text(translations.settingsUtilsResetDefaultsName),
|
||||
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
|
||||
content: Button(
|
||||
onPressed: () => showResetDialog(_settingsController.reset),
|
||||
child: Text(translations.settingsUtilsResetDefaultsContent),
|
||||
)
|
||||
);
|
||||
|
||||
SettingTile get _installationDirectory => SettingTile(
|
||||
icon: Icon(
|
||||
FluentIcons.folder_24_regular
|
||||
@@ -97,15 +112,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
|
||||
child: Text(translations.settingsUtilsInstallationDirectoryContent),
|
||||
)
|
||||
);
|
||||
|
||||
String _getLocaleName(String locale) {
|
||||
var result = LocaleNames.of(context)!.nameOf(locale);
|
||||
if(result != null) {
|
||||
return "${result.substring(0, 1).toUpperCase()}${result.substring(1).toLowerCase()}";
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
|
||||
extension _ThemeModeExtension on ThemeMode {
|
||||
|
||||
Reference in New Issue
Block a user