mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 11:12:23 +01:00
359 lines
11 KiB
Dart
359 lines
11 KiB
Dart
|
|
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: WidgetStateProperty.all(Colors.transparent),
|
|
shape: WidgetStateProperty.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;
|
|
}
|
|
}
|
|
} |