diff --git a/lib/src/screens/home/home.dart b/lib/src/screens/home/home.dart index 320593a..b24e6a9 100644 --- a/lib/src/screens/home/home.dart +++ b/lib/src/screens/home/home.dart @@ -27,6 +27,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:steamwar_multitool/src/provider/provider.dart'; import 'package:steamwar_multitool/src/screens/home/lists/events_list.dart'; +import 'lists/mod_list.dart'; + class HomeScreen extends HookConsumerWidget { const HomeScreen({Key? key}) : super(key: key); @@ -79,7 +81,6 @@ class HomeScreen extends HookConsumerWidget { label: Text("Mods"), selectedIcon: Icon(Icons.developer_mode), ), - const Spacer(), const NavigationDrawerDestination( icon: Icon(Icons.settings_outlined), label: Text("Settings"), @@ -129,17 +130,12 @@ class HomeScreen extends HookConsumerWidget { ), ], ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: IndexedStack( - index: navRailIndex.value, - children: const [ - //ServerListComponent(), - EventListComponent(), - Placeholder(), - //ModListComponent(), - ], - ), + body: IndexedStack( + index: navRailIndex.value, + children: const [ + EventListComponent(), + ModListComponent(), + ], ), ); } diff --git a/lib/src/screens/home/lists/events_list.dart b/lib/src/screens/home/lists/events_list.dart index 58c8570..fb1617b 100644 --- a/lib/src/screens/home/lists/events_list.dart +++ b/lib/src/screens/home/lists/events_list.dart @@ -38,53 +38,56 @@ class EventListComponent extends HookConsumerWidget { final pastEvents = data.where((element) => !element.isUpcoming && !element.isCurrent); final currentEvent = data.indexWhere((element) => element.isCurrent); - return ListView( - children: [ - Row( - children: [ - FilledButton.icon( - onPressed: () { - showModalBottomSheet( - constraints: BoxConstraints(maxWidth: 500), - context: context, - builder: (context) => const CreateEventDialog()); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListView( + children: [ + Row( + children: [ + FilledButton.icon( + onPressed: () { + showModalBottomSheet( + constraints: BoxConstraints(maxWidth: 500), + context: context, + builder: (context) => const CreateEventDialog()); + }, + label: const Text("Create Event"), + icon: const Icon(Icons.add), + ), + ], + ), + const SizedBox( + height: 16, + ), + if (currentEvent != -1) + ListTile( + title: Text( + data[currentEvent].name, + ), + subtitle: const Text( + "Current Event", + ), + leading: const Icon(Icons.priority_high), + onTap: () { + context.go("/event/${data[currentEvent].id}"); }, - label: const Text("Create Event"), - icon: const Icon(Icons.add), ), - ], - ), - const SizedBox( - height: 16, - ), - if (currentEvent != -1) - ListTile( - title: Text( - data[currentEvent].name, + if (upcomingEvents.isNotEmpty) + Text( + "Upcoming Events", + style: Theme.of(context).textTheme.headlineSmall, ), - subtitle: const Text( - "Current Event", + ...upcomingEvents.map( + (e) => EventTile(e), + ), + if (pastEvents.isNotEmpty) + Text( + "Past Events", + style: Theme.of(context).textTheme.headlineSmall, ), - leading: const Icon(Icons.priority_high), - onTap: () { - context.go("/event/${data[currentEvent].id}"); - }, - ), - if (upcomingEvents.isNotEmpty) - Text( - "Upcoming Events", - style: Theme.of(context).textTheme.headlineSmall, - ), - ...upcomingEvents.map( - (e) => EventTile(e), - ), - if (pastEvents.isNotEmpty) - Text( - "Past Events", - style: Theme.of(context).textTheme.headlineSmall, - ), - ...pastEvents.map((e) => EventTile(e)), - ], + ...pastEvents.map((e) => EventTile(e)), + ], + ), ); }, error: (err, stack) { return ErrorComponent(err, stack); diff --git a/lib/src/screens/home/lists/mod_list.dart b/lib/src/screens/home/lists/mod_list.dart index 19bd4c7..509ecd2 100644 --- a/lib/src/screens/home/lists/mod_list.dart +++ b/lib/src/screens/home/lists/mod_list.dart @@ -18,36 +18,296 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:steamwar_multitool/src/components/error.dart'; import 'package:steamwar_multitool/src/provider/mods.dart'; +import 'package:steamwar_multitool/src/types/mods.dart'; +import 'package:url_launcher/link.dart'; class ModListComponent extends HookConsumerWidget { const ModListComponent({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final list = ref.watch(uncheckedMods); + final tabController = useTabController(initialLength: 2); - return list.when( + return Column( + children: [ + TabBar( + tabs: [ + const Tab( + text: "Unrated", + ), + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.search), + Text("Search"), + ], + ), + ) + ], + controller: tabController, + ), + Expanded( + child: TabBarView( + controller: tabController, + children: const [ + UnratedModsList(), + SearchModsList(), + ], + ), + ), + ], + ); + } +} + +class UnratedModsList extends HookConsumerWidget { + const UnratedModsList({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mods = ref.watch(uncheckedMods); + var i = 0; + + return mods.when( data: (data) { return ListView( + shrinkWrap: true, children: [ - Center( - child: Text("WIP", - style: Theme.of(context).textTheme.headline5)), - ...data - .map( - (e) => ListTile( - title: Text(e.name), - subtitle: Text(e.platform.name), + for (final mod in data) + ExpansionTile( + title: Text(mod.name), + subtitle: + Text("Modloader: ${mod.platform.name.capitalize()}"), + initiallyExpanded: i++ == 0, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://modrinth.com/mods?q=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Modrinth"), + icon: const Icon(Icons.open_in_new), + ); + }), + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://beta.curseforge.com/minecraft/search?search=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Curseforge"), + icon: const Icon(Icons.open_in_new), + ); + }), + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://github.com/search?q=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Github"), + icon: const Icon(Icons.open_in_new), + ); + }), + ], ), - ) - .toList() + const Divider(), + SegmentedButton( + segments: [ + for (final type in ModType.values) + ButtonSegment( + value: type, + label: Text(type.name.capitalize()), + ) + ], + selected: {mod.type}, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + onSelectionChanged: (value) async { + final repo = await ref.read(modRepository.future); + await repo.updateMod( + mod.platform, mod.name, value.first); + ref.invalidate(uncheckedMods); + }, + ), + const SizedBox(height: 10), + ], + ), ], ); }, error: (err, stack) => ErrorComponent(err, stack), - loading: () => const Center(child: CircularProgressIndicator())); + loading: () => const LinearProgressIndicator()); + } +} + +class SearchModsList extends HookConsumerWidget { + const SearchModsList({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final modsList = ref.watch(mods); + final nameFilter = useState(""); + final platformFilter = useState(Platform.values.toSet()); + final typeFilter = useState(ModType.values.toSet()); + + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: "Name", + ), + onChanged: (value) { + nameFilter.value = value; + }, + ), + const SizedBox(height: 10), + SegmentedButton( + segments: [ + for (final platform in Platform.values) + ButtonSegment( + value: platform, + label: Text(platform.name.capitalize()), + ) + ], + selected: platformFilter.value, + multiSelectionEnabled: true, + emptySelectionAllowed: false, + onSelectionChanged: (value) { + platformFilter.value = value; + }, + ), + const SizedBox(height: 10), + SegmentedButton( + segments: [ + for (final type in ModType.values) + ButtonSegment( + value: type, + label: Text(type.name.capitalize()), + ) + ], + selected: typeFilter.value, + multiSelectionEnabled: true, + emptySelectionAllowed: false, + onSelectionChanged: (value) { + typeFilter.value = value; + }, + ), + ], + ), + ), + const Divider(), + ...modsList.when( + data: (data) { + final filtered = data + .where((mod) => + (nameFilter.value.isEmpty || + mod.name + .toLowerCase() + .contains(nameFilter.value.toLowerCase())) && + platformFilter.value.contains(mod.platform) && + typeFilter.value.contains(mod.type)) + .toList(); + + return filtered.map((mod) { + return ExpansionTile( + title: Text(mod.name), + subtitle: + Text("Modloader: ${mod.platform.name.capitalize()}"), + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://modrinth.com/mods?q=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Modrinth"), + icon: const Icon(Icons.open_in_new), + ); + }), + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://beta.curseforge.com/minecraft/search?search=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Curseforge"), + icon: const Icon(Icons.open_in_new), + ); + }), + Link( + target: LinkTarget.blank, + uri: Uri.tryParse( + "https://github.com/search?q=${mod.name}"), + builder: (context, followLink) { + return FilledButton.tonalIcon( + onPressed: followLink, + label: const Text("Github"), + icon: const Icon(Icons.open_in_new), + ); + }), + ], + ), + const Divider(), + SegmentedButton( + segments: [ + for (final type in ModType.values) + ButtonSegment( + value: type, + label: Text(type.name.capitalize()), + ) + ], + selected: {mod.type}, + multiSelectionEnabled: false, + emptySelectionAllowed: false, + onSelectionChanged: (value) async { + final repo = await ref.read(modRepository.future); + await repo.updateMod( + mod.platform, mod.name, value.first); + ref.invalidate(mods); + }, + ), + const SizedBox(height: 10), + ], + ); + }); + }, + error: (err, stack) => [ErrorComponent(err, stack)], + loading: () => const [LinearProgressIndicator()]) + ], + ); + } +} + +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; } } diff --git a/pubspec.lock b/pubspec.lock index 6131a27..ae5b308 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -292,10 +292,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: feab99a20fd248c658c923ba98f4449ca6e575c3dee9fdf07146f4f33482c6bc + sha256: "50bc08b72ede07daaf897deb4c00ca8fd8976c8821a4c84d7aeef0e92d1c5620" url: "https://pub.dev" source: hosted - version: "6.5.5" + version: "6.5.6" graphs: dependency: transitive description: @@ -540,10 +540,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_linux: dependency: transitive description: @@ -705,18 +705,18 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411 url: "https://pub.dev" source: hosted - version: "6.0.26" + version: "6.0.27" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.1.4" url_launcher_linux: dependency: transitive description: @@ -729,10 +729,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: @@ -777,18 +777,18 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: