From ff6be4e41bcda82393f66a3676580767ad4998b7 Mon Sep 17 00:00:00 2001 From: SlayerOrnstein <6075693+SlayerOrnstein@users.noreply.github.com> Date: Sat, 21 Dec 2024 16:44:03 -0500 Subject: [PATCH 1/2] feat: add mastery tracking --- devtools_options.yaml | 4 + lib/app/view/app_page.dart | 2 +- lib/app/view/app_root.dart | 11 + lib/app/widgets/bloc_bootstrap.dart | 13 +- lib/app/widgets/repo_bootstrap.dart | 7 +- lib/arsenal/arsenal.dart | 2 + lib/arsenal/cubit/arsenal_cubit.dart | 32 + lib/arsenal/cubit/arsenal_state.dart | 49 + lib/arsenal/widgets/arsenal_item.dart | 33 + lib/arsenal/widgets/widgets.dart | 1 + lib/codex/cubit/item_cubit.dart | 4 + lib/codex/views/codex_search_view.dart | 8 +- lib/codex/widgets/codex_entry/gun_stats.dart | 2 +- .../widgets/codex_entry/melee_stats.dart | 2 +- lib/home/views/home.dart | 25 +- lib/home/widgets/mastery_section.dart | 49 + lib/home/widgets/widgets.dart | 1 + lib/l10n/arb/intl_en.arb | 48 + lib/router/routes.dart | 8 +- lib/settings/cubit/user_settings_cubit.dart | 38 +- lib/settings/cubit/user_settings_state.dart | 10 +- lib/settings/utils/settings_keys.dart | 3 + lib/settings/utils/user_settings.dart | 11 +- lib/settings/views/settings.dart | 44 +- lib/settings/widgets/username_input.dart | 128 ++ lib/settings/widgets/widgets.dart | 1 + .../.github/ISSUE_TEMPLATE/bug_report.md | 29 - .../.github/ISSUE_TEMPLATE/build.md | 14 - .../.github/ISSUE_TEMPLATE/chore.md | 14 - .../.github/ISSUE_TEMPLATE/ci.md | 14 - .../.github/ISSUE_TEMPLATE/config.yml | 1 - .../.github/ISSUE_TEMPLATE/documentation.md | 14 - .../.github/ISSUE_TEMPLATE/feature_request.md | 18 - .../.github/ISSUE_TEMPLATE/performance.md | 14 - .../.github/ISSUE_TEMPLATE/refactor.md | 14 - .../.github/ISSUE_TEMPLATE/revert.md | 16 - .../.github/ISSUE_TEMPLATE/style.md | 14 - .../.github/ISSUE_TEMPLATE/test.md | 14 - .../.github/PULL_REQUEST_TEMPLATE.md | 27 - packages/fish_repository/.github/cspell.json | 21 - .../fish_repository/.github/dependabot.yaml | 11 - .../.github/workflows/main.yaml | 25 - packages/navis_ui/pubspec.lock | 42 +- .../lib/src/arsenal_database.dart | 88 ++ .../lib/src/arsenal_database.g.dart | 1316 +++++++++++++++++ .../lib/src/converters/converters.dart | 1 + .../src/converters/item_type_converter.dart | 18 + .../lib/src/repository.dart | 63 +- .../lib/src/tables/arsenal_item.dart | 23 + .../lib/src/tables/arsenal_manifest.dart | 6 + .../lib/src/tables/arsenal_xp_item.dart | 11 + .../lib/src/tables/tables.dart | 3 + .../lib/src/utils/extensions.dart | 29 + .../lib/src/utils/mastery_progress.dart | 42 + .../lib/src/utils/utils.dart | 2 + .../lib/warframestat_repository.dart | 2 + packages/warframestat_repository/pubspec.yaml | 6 +- .../test/warframestat_repository_test.dart | 8 +- pubspec.lock | 54 +- pubspec.yaml | 3 +- 60 files changed, 2162 insertions(+), 351 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/arsenal/arsenal.dart create mode 100644 lib/arsenal/cubit/arsenal_cubit.dart create mode 100644 lib/arsenal/cubit/arsenal_state.dart create mode 100644 lib/arsenal/widgets/arsenal_item.dart create mode 100644 lib/arsenal/widgets/widgets.dart create mode 100644 lib/home/widgets/mastery_section.dart create mode 100644 lib/settings/widgets/username_input.dart delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/build.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/chore.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/ci.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/config.yml delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/documentation.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/performance.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/refactor.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/revert.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/style.md delete mode 100644 packages/fish_repository/.github/ISSUE_TEMPLATE/test.md delete mode 100644 packages/fish_repository/.github/PULL_REQUEST_TEMPLATE.md delete mode 100644 packages/fish_repository/.github/cspell.json delete mode 100644 packages/fish_repository/.github/dependabot.yaml delete mode 100644 packages/fish_repository/.github/workflows/main.yaml create mode 100644 packages/warframestat_repository/lib/src/arsenal_database.dart create mode 100644 packages/warframestat_repository/lib/src/arsenal_database.g.dart create mode 100644 packages/warframestat_repository/lib/src/converters/converters.dart create mode 100644 packages/warframestat_repository/lib/src/converters/item_type_converter.dart create mode 100644 packages/warframestat_repository/lib/src/tables/arsenal_item.dart create mode 100644 packages/warframestat_repository/lib/src/tables/arsenal_manifest.dart create mode 100644 packages/warframestat_repository/lib/src/tables/arsenal_xp_item.dart create mode 100644 packages/warframestat_repository/lib/src/tables/tables.dart create mode 100644 packages/warframestat_repository/lib/src/utils/extensions.dart create mode 100644 packages/warframestat_repository/lib/src/utils/mastery_progress.dart create mode 100644 packages/warframestat_repository/lib/src/utils/utils.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..f592d85a9 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - drift: true \ No newline at end of file diff --git a/lib/app/view/app_page.dart b/lib/app/view/app_page.dart index 380649197..4ecd2cfc8 100644 --- a/lib/app/view/app_page.dart +++ b/lib/app/view/app_page.dart @@ -32,7 +32,7 @@ class AppView extends StatelessWidget { child: child, ); }, - child: SafeArea(child: children[currentIndex]), + child: children[currentIndex], ), bottomNavigationBar: NavigationBar( onDestinationSelected: (i) { diff --git a/lib/app/view/app_root.dart b/lib/app/view/app_root.dart index 0b5d012bf..fec2ed56d 100644 --- a/lib/app/view/app_root.dart +++ b/lib/app/view/app_root.dart @@ -4,6 +4,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:feedback_sentry/feedback_sentry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/arsenal/arsenal.dart'; import 'package:navis/l10n/l10n.dart'; import 'package:navis/router/app_router.dart'; import 'package:navis/settings/settings.dart'; @@ -106,6 +107,16 @@ class _NavisAppState extends State with WidgetsBindingObserver { super.didChangeDependencies(); BlocProvider.of(context).fetchWorldstate(); + + final settings = context.read().state; + final username = switch (settings) { + UserSettingsSuccess() => settings.username, + _ => null, + }; + + if (username != null) { + BlocProvider.of(context).updateArsenal(username); + } } @override diff --git a/lib/app/widgets/bloc_bootstrap.dart b/lib/app/widgets/bloc_bootstrap.dart index 2b785c107..31dc6d07d 100644 --- a/lib/app/widgets/bloc_bootstrap.dart +++ b/lib/app/widgets/bloc_bootstrap.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/arsenal/cubit/arsenal_cubit.dart'; import 'package:navis/settings/settings.dart'; import 'package:navis/worldstate/worldstate.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; @@ -16,17 +17,18 @@ class BlocBootstrap extends StatefulWidget { class _BlocBootstrapState extends State { late WorldstateCubit _worldstateCubit; late UserSettingsCubit _userSettingsCubit; + late ArsenalCubit _arsenalCubit; @override void initState() { super.initState(); - final worldstateRepo = - RepositoryProvider.of(context); - final usersettings = RepositoryProvider.of(context); + final wsRepo = RepositoryProvider.of(context); + final settings = RepositoryProvider.of(context); - _worldstateCubit = WorldstateCubit(worldstateRepo); - _userSettingsCubit = UserSettingsCubit(usersettings); + _worldstateCubit = WorldstateCubit(wsRepo); + _userSettingsCubit = UserSettingsCubit(settings); + _arsenalCubit = ArsenalCubit(wsRepo); } @override @@ -35,6 +37,7 @@ class _BlocBootstrapState extends State { providers: [ BlocProvider.value(value: _worldstateCubit), BlocProvider.value(value: _userSettingsCubit), + BlocProvider.value(value: _arsenalCubit), ], child: widget.child, ); diff --git a/lib/app/widgets/repo_bootstrap.dart b/lib/app/widgets/repo_bootstrap.dart index b08f6ee81..e308b4f72 100644 --- a/lib/app/widgets/repo_bootstrap.dart +++ b/lib/app/widgets/repo_bootstrap.dart @@ -1,3 +1,4 @@ +import 'package:drift_flutter/drift_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hive/hive.dart'; @@ -31,8 +32,10 @@ class _RepositoryBootstrapState extends State { super.initState(); _notifications = NotificationRepository(); - _warframestatRepository = - WarframestatRepository(client: SentryHttpClient()); + _warframestatRepository = WarframestatRepository( + client: SentryHttpClient(), + database: ArsenalDatabase(driftDatabase(name: 'arsenal')), + ); } @override diff --git a/lib/arsenal/arsenal.dart b/lib/arsenal/arsenal.dart new file mode 100644 index 000000000..c6f08fa11 --- /dev/null +++ b/lib/arsenal/arsenal.dart @@ -0,0 +1,2 @@ +export 'cubit/arsenal_cubit.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/arsenal/cubit/arsenal_cubit.dart b/lib/arsenal/cubit/arsenal_cubit.dart new file mode 100644 index 000000000..b7170a731 --- /dev/null +++ b/lib/arsenal/cubit/arsenal_cubit.dart @@ -0,0 +1,32 @@ +import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; + +part 'arsenal_state.dart'; + +class ArsenalCubit extends Cubit { + ArsenalCubit(this.repository) : super(ArsenalInitial()); + + final WarframestatRepository repository; + + late List _xpInfo; + + Future updateArsenal(String username, {bool update = false}) async { + try { + emit(ArsenalUpdating()); + await repository.updateArsenalItems(update: update); + + _xpInfo = await repository.syncXpInfo(username); + + emit(ArsenalSuccess(_xpInfo)); + } catch (e, stack) { + debugPrint(e.toString()); + emit(ArsenalFailure()); + + await Sentry.captureException(e, stackTrace: stack); + } + } +} diff --git a/lib/arsenal/cubit/arsenal_state.dart b/lib/arsenal/cubit/arsenal_state.dart new file mode 100644 index 000000000..e3661f631 --- /dev/null +++ b/lib/arsenal/cubit/arsenal_state.dart @@ -0,0 +1,49 @@ +part of 'arsenal_cubit.dart'; + +sealed class ArsenalState extends Equatable { + const ArsenalState(); + + @override + List get props => []; +} + +final class ArsenalInitial extends ArsenalState {} + +final class ArsenalUpdating extends ArsenalState {} + +final class ArsenalSuccess extends ArsenalState { + const ArsenalSuccess(this.xpInfo); + + final List xpInfo; + + List get inProgress { + return xpInfo.where((i) => i.rank < i.maxRank).toList() + ..sort((a, b) { + if (a.rank == 0 && b.rank == 0) return 0; + if (a.rank == 0) return 1; + if (b.rank == 0) return -1; + + return a.rank.compareTo(b.rank); + }); + } + + List get warframes => + xpInfo.whereNot((i) => i.item.type.isWeapon).toList(); + + List get weapons => + xpInfo.where((i) => i.item.type.isWeapon).toList(); + + List get primaries => + xpInfo.where((i) => i.item.type.isPrimary).toList(); + + List get secondary => + xpInfo.where((i) => i.item.type.isSecondary).toList(); + + List get melee => + xpInfo.where((i) => i.item.type.isMelee).toList(); + + @override + List get props => [xpInfo]; +} + +final class ArsenalFailure extends ArsenalState {} diff --git a/lib/arsenal/widgets/arsenal_item.dart b/lib/arsenal/widgets/arsenal_item.dart new file mode 100644 index 000000000..a358f6ffb --- /dev/null +++ b/lib/arsenal/widgets/arsenal_item.dart @@ -0,0 +1,33 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:navis/utils/item_extensions.dart'; +import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; + +class ArsenalItemWidget extends StatelessWidget { + const ArsenalItemWidget({super.key, required this.arsenalItem}); + + final MasteryProgress arsenalItem; + + @override + Widget build(BuildContext context) { + return AppCard( + child: ListTile( + leading: CachedNetworkImage( + imageUrl: arsenalItem.item.imageUrl, + width: 50, + ), + title: Text(arsenalItem.item.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Rank ${arsenalItem.rank}'), + LinearProgressIndicator( + value: arsenalItem.rank / arsenalItem.maxRank, + ), + ], + ), + ), + ); + } +} diff --git a/lib/arsenal/widgets/widgets.dart b/lib/arsenal/widgets/widgets.dart new file mode 100644 index 000000000..a47d7ae84 --- /dev/null +++ b/lib/arsenal/widgets/widgets.dart @@ -0,0 +1 @@ +export 'arsenal_item.dart'; diff --git a/lib/codex/cubit/item_cubit.dart b/lib/codex/cubit/item_cubit.dart index 257f4a774..ba0b575fa 100644 --- a/lib/codex/cubit/item_cubit.dart +++ b/lib/codex/cubit/item_cubit.dart @@ -18,6 +18,10 @@ class ItemCubit extends HydratedCubit { Future fetchItem() async { final item = await _handleItemFetch(() async => repo.fetchItem(name)); + if (item == null) { + emit(const ItemFetchFailure('Item does not exist')); + return; + } emit(ItemFetchSuccess(item)); } diff --git a/lib/codex/views/codex_search_view.dart b/lib/codex/views/codex_search_view.dart index 2e0a8ad72..3410cb8d0 100644 --- a/lib/codex/views/codex_search_view.dart +++ b/lib/codex/views/codex_search_view.dart @@ -27,13 +27,13 @@ class CodexSearchPage extends StatelessWidget { context, ), sliver: SliverAppBar( + titleSpacing: 0, + floating: true, + scrolledUnderElevation: 0, + automaticallyImplyLeading: false, clipBehavior: Clip.none, shape: const StadiumBorder(), - scrolledUnderElevation: 0, - titleSpacing: 0, backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - floating: true, forceElevated: innerBoxIsScrolled, title: const CodexSearchBar(), ), diff --git a/lib/codex/widgets/codex_entry/gun_stats.dart b/lib/codex/widgets/codex_entry/gun_stats.dart index 4af36bab4..6ea3d555e 100644 --- a/lib/codex/widgets/codex_entry/gun_stats.dart +++ b/lib/codex/widgets/codex_entry/gun_stats.dart @@ -31,7 +31,7 @@ class GunStats extends StatelessWidget { ), RowItem( text: Text(l10n.weaponTypeTitle), - child: Text(gun.type.category), + child: Text(gun.type.type), ), if (gun.polarities?.isNotEmpty ?? false) RowItem( diff --git a/lib/codex/widgets/codex_entry/melee_stats.dart b/lib/codex/widgets/codex_entry/melee_stats.dart index 1f312cfed..732330a40 100644 --- a/lib/codex/widgets/codex_entry/melee_stats.dart +++ b/lib/codex/widgets/codex_entry/melee_stats.dart @@ -33,7 +33,7 @@ class MeleeStats extends StatelessWidget { ), RowItem( text: Text(l10n.weaponTypeTitle), - child: Text(melee.type.category), + child: Text(melee.type.type), ), if (melee.stancePolarity != null) RowItem( diff --git a/lib/home/views/home.dart b/lib/home/views/home.dart index 46ecbd220..5a66ab143 100644 --- a/lib/home/views/home.dart +++ b/lib/home/views/home.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:navis/home/widgets/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/home/home.dart'; +import 'package:navis/settings/settings.dart'; import 'package:navis_ui/navis_ui.dart'; class HomePage extends StatelessWidget { @@ -16,8 +18,25 @@ class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView( - children: const [NewsSection(), Gaps.gap16, ActivitiesSection()], + return BlocBuilder( + builder: (context, state) { + final username = switch (state) { + UserSettingsSuccess() => state.username, + _ => null + }; + + final children = [ + const NewsSection(), + const ActivitiesSection(), + if (username != null) const MasteryInProgressSection(), + ]; + + return ListView.separated( + itemCount: children.length, + separatorBuilder: (_, __) => Gaps.gap16, + itemBuilder: (context, index) => children[index], + ); + }, ); } } diff --git a/lib/home/widgets/mastery_section.dart b/lib/home/widgets/mastery_section.dart new file mode 100644 index 000000000..70af97911 --- /dev/null +++ b/lib/home/widgets/mastery_section.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/arsenal/arsenal.dart'; +import 'package:navis/home/widgets/section.dart'; +import 'package:navis_ui/navis_ui.dart'; + +class MasteryInProgressSection extends StatelessWidget { + const MasteryInProgressSection({super.key}); + + @override + Widget build(BuildContext context) { + return Section( + title: const Text('Mastery in progress'), + content: BlocBuilder( + builder: (context, state) { + const padding = EdgeInsets.symmetric(vertical: 16); + + if (state is ArsenalUpdating) { + return const Padding( + padding: padding, + child: Text('Updating XP info'), + ); + } + + if (state is ArsenalFailure) { + return const Padding( + padding: padding, + child: Text('Error updating XP info'), + ); + } + + if (state is! ArsenalSuccess) { + return const Padding( + padding: padding, + child: WarframeSpinner(size: 100), + ); + } + + return Column( + children: [ + for (final i in state.inProgress.take(5)) + ArsenalItemWidget(arsenalItem: i), + ], + ); + }, + ), + ); + } +} diff --git a/lib/home/widgets/widgets.dart b/lib/home/widgets/widgets.dart index ef5e84eba..8ddfb38f1 100644 --- a/lib/home/widgets/widgets.dart +++ b/lib/home/widgets/widgets.dart @@ -1,2 +1,3 @@ export 'activities_section.dart'; +export 'mastery_section.dart'; export 'news_section.dart'; diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 3d0b9ef53..844725c52 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -1254,5 +1254,53 @@ "description": "Activities title", "type": "text", "placeholder": {} + }, + "cancelText": "Cancel", + "@cancelText": { + "description": "Text to display for cancel buttons", + "type": "text", + "placeholder": {} + }, + "syncingInfoText": "Syncing XP info", + "@syncingInfoText": { + "description": "Text displayed when xp info is being synced", + "type": "text", + "placeholder": {} + }, + "updateManifestTitle": "Update manifest", + "@updateManifestTitle": { + "description": "Text displayed under settings", + "type": "text", + "placeholder": {} + }, + "updateManifestSubtitle": "Update internal manifest of masterable items", + "@updateManifestSubtitle": { + "description": "Text displayed under updateManifestTitle", + "type": "text", + "placeholder": {} + }, + "updatingManifestText": "Updating manifest", + "@updatinManifest": { + "description": "Text displayed when the manifest is updating", + "type": "text", + "placeholder": {} + }, + "masteryTrackingCategoryTitle": "Mastery tracking", + "@masteryTrackingCategoryTitle": { + "description": "Text displayed for mastery related settings", + "type": "text", + "placeholder": {} + }, + "enterUsernameHintText": "Enter username", + "@enterUsernameHintText": { + "description": "Hint Text use as a placeholder in username input", + "type": "text", + "placeholder": {} + }, + "clearUsernameButtonLabel": "Clear username", + "@clearUsernameButtonLabel": { + "description": "Label text for clear username button", + "type": "text", + "placeholder": {} } } \ No newline at end of file diff --git a/lib/router/routes.dart b/lib/router/routes.dart index f329af6de..1b397b2fd 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -80,7 +80,7 @@ class ActivitesPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const ActivitiesView(); + return const SafeArea(child: ActivitiesView()); } } @@ -95,7 +95,7 @@ class OverviewPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const HomePage(); + return const SafeArea(child: HomePage()); } } @@ -110,7 +110,7 @@ class ExplorePageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const ExplorePage(); + return const SafeArea(child: ExplorePage()); } } @@ -125,7 +125,7 @@ class SettingsPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const SettingsPage(); + return const SafeArea(child: SettingsPage()); } } diff --git a/lib/settings/cubit/user_settings_cubit.dart b/lib/settings/cubit/user_settings_cubit.dart index 9a4e1f40b..de6369c5a 100644 --- a/lib/settings/cubit/user_settings_cubit.dart +++ b/lib/settings/cubit/user_settings_cubit.dart @@ -9,12 +9,28 @@ part 'user_settings_state.dart'; class UserSettingsCubit extends Cubit { UserSettingsCubit(UserSettings settings) : _settings = settings, - super(UserSettingsInitial()) { - _initSettings(); - } + super( + UserSettingsSuccess( + username: settings.username, + language: settings.language, + themeMode: settings.theme, + isOptOut: settings.isOptOut, + isFirstTime: settings.isFirstTime, + toggles: { + for (final topic in Topics.topics) ...{ + topic.name: settings.getToggle(topic.name), + }, + }, + ), + ); final UserSettings _settings; + void updateUsername(String? username) { + _settings.username = username; + emit((state as UserSettingsSuccess).copyWith(username: _settings.username)); + } + void updateLanguage(Locale language) { _settings.language = language; emit((state as UserSettingsSuccess).copyWith(language: _settings.language)); @@ -39,20 +55,4 @@ class UserSettingsCubit extends Cubit { emit(settings.copyWith(toggles: toggles)); } - - void _initSettings() { - final settings = UserSettingsSuccess( - language: _settings.language, - themeMode: _settings.theme, - isOptOut: _settings.isOptOut, - isFirstTime: _settings.isFirstTime, - toggles: { - for (final topic in Topics.topics) ...{ - topic.name: _settings.getToggle(topic.name), - }, - }, - ); - - emit(settings); - } } diff --git a/lib/settings/cubit/user_settings_state.dart b/lib/settings/cubit/user_settings_state.dart index db44c1431..7559ea1d0 100644 --- a/lib/settings/cubit/user_settings_state.dart +++ b/lib/settings/cubit/user_settings_state.dart @@ -4,13 +4,14 @@ sealed class UserSettingsState extends Equatable { const UserSettingsState(); @override - List get props => []; + List get props => []; } final class UserSettingsInitial extends UserSettingsState {} final class UserSettingsSuccess extends UserSettingsState { const UserSettingsSuccess({ + required this.username, required this.language, required this.themeMode, required this.isOptOut, @@ -18,6 +19,7 @@ final class UserSettingsSuccess extends UserSettingsState { required this.toggles, }); + final String? username; final Locale language; final ThemeMode themeMode; final bool isOptOut; @@ -25,6 +27,7 @@ final class UserSettingsSuccess extends UserSettingsState { final Map toggles; UserSettingsSuccess copyWith({ + String? username, Locale? language, ThemeMode? themeMode, bool? isOptOut, @@ -32,6 +35,7 @@ final class UserSettingsSuccess extends UserSettingsState { Map? toggles, }) { return UserSettingsSuccess( + username: username, language: language ?? this.language, themeMode: themeMode ?? this.themeMode, isOptOut: isOptOut ?? this.isOptOut, @@ -41,8 +45,8 @@ final class UserSettingsSuccess extends UserSettingsState { } @override - List get props => - [language, themeMode, isOptOut, isFirstTime, toggles]; + List get props => + [language, themeMode, isOptOut, isFirstTime, toggles, username]; } final class UserSettingsFailure extends UserSettingsState {} diff --git a/lib/settings/utils/settings_keys.dart b/lib/settings/utils/settings_keys.dart index fbc73ea83..e12a0a34c 100644 --- a/lib/settings/utils/settings_keys.dart +++ b/lib/settings/utils/settings_keys.dart @@ -10,4 +10,7 @@ class SettingsKeys { /// The key where the user's set ThemeMode is stored as a String. static const String theme = 'theme'; + + /// Username key + static const String username = 'username'; } diff --git a/lib/settings/utils/user_settings.dart b/lib/settings/utils/user_settings.dart index 11b22148f..6d1b33158 100644 --- a/lib/settings/utils/user_settings.dart +++ b/lib/settings/utils/user_settings.dart @@ -1,4 +1,4 @@ -import 'dart:developer'; +import 'dart:developer' as developer; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -21,12 +21,18 @@ class UserSettings { /// /// You must call [Hive.init(path)] before calling this function static Future initSettings(String path) async { - log('Initializing Usersettings Hive'); + developer.log('initializing user settings'); final box = await SentryHive.openBox('user_settings', path: path); return _instance ??= UserSettings._(box); } + String? get username => + _userSettingsBox.get(SettingsKeys.username) as String?; + + set username(String? username) => + _userSettingsBox.put(SettingsKeys.username, username); + /// Returns the stored language as a locale. Locale get language { final value = _userSettingsBox.get(SettingsKeys.userLanguage) as String?; @@ -41,7 +47,6 @@ class UserSettings { /// Updates the stored [UserSettings.language]. set language(Locale? value) { if (value != null) { - log('setting new lang ${value.languageCode}'); _userSettingsBox.put(SettingsKeys.userLanguage, value.languageCode); } } diff --git a/lib/settings/views/settings.dart b/lib/settings/views/settings.dart index f878ed34f..476102372 100644 --- a/lib/settings/views/settings.dart +++ b/lib/settings/views/settings.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:intl/intl.dart'; +import 'package:navis/arsenal/cubit/arsenal_cubit.dart'; import 'package:navis/l10n/l10n.dart'; import 'package:navis/settings/settings.dart'; import 'package:navis_ui/navis_ui.dart'; @@ -12,14 +13,9 @@ import 'package:notification_repository/notification_repository.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); - static const route = '/settings'; - @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Settings')), - body: const SafeArea(child: _SettingsView()), - ); + return const _SettingsView(); } } @@ -47,12 +43,32 @@ class _SettingsView extends StatelessWidget { FilterDialog.showFilters(context, filters); } + Future _forceUpdateManifest( + BuildContext context, + String? username, + ) async { + if (!context.mounted) return; + if (username == null) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.updatingManifestText)), + ); + + await BlocProvider.of(context) + .updateArsenal(username, update: true); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; final filters = NotificationTopics(context.l10n); final settings = context.watch().state; + final username = switch (settings) { + UserSettingsSuccess() => settings.username, + _ => null + }; + final themeMode = switch (settings) { UserSettingsSuccess() => settings.themeMode, _ => ThemeMode.system @@ -77,6 +93,22 @@ class _SettingsView extends StatelessWidget { lightTheme: theme, darkTheme: theme, sections: [ + SettingsSection( + title: Text(l10n.masteryTrackingCategoryTitle), + tiles: [ + SettingsTile( + title: Text(username ?? l10n.enterUsernameHintText), + // TODO(Orn): find a way to show mastery progress + onPressed: UsernameInput.show, + ), + SettingsTile( + title: Text(l10n.updateManifestTitle), + description: Text(l10n.updateManifestSubtitle), + enabled: username != null, + onPressed: (context) => _forceUpdateManifest(context, username), + ), + ], + ), SettingsSection( title: Text(l10n.behaviorTitle), tiles: [ diff --git a/lib/settings/widgets/username_input.dart b/lib/settings/widgets/username_input.dart new file mode 100644 index 000000000..6582ca36c --- /dev/null +++ b/lib/settings/widgets/username_input.dart @@ -0,0 +1,128 @@ +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/arsenal/arsenal.dart'; +import 'package:navis/l10n/l10n.dart'; +import 'package:navis/settings/settings.dart'; +import 'package:navis_ui/navis_ui.dart'; + +class UsernameInput extends StatefulWidget { + const UsernameInput({super.key, this.username}); + + final String? username; + + static Future show(BuildContext context) { + return showModalBottomSheet( + context: context, + builder: (context) { + final username = context.select((UserSettingsCubit cubit) { + final state = cubit.state; + if (state is! UserSettingsSuccess) return null; + + return state.username; + }); + + return UsernameInput(username: username); + }, + ); + } + + @override + State createState() => _UsernameInputState(); +} + +class _UsernameInputState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + bool get _isValid { + return _controller.text.isNotEmpty && _controller.text.length >= 4; + } + + void _onSubmit([String? value]) { + final username = value ?? _controller.text; + + BlocProvider.of(context).updateUsername(username); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(context.l10n.syncingInfoText))); + + BlocProvider.of(context).updateArsenal(username); + Navigator.pop(context); + } + + void _onClear() { + BlocProvider.of(context).updateUsername(null); + } + + @override + Widget build(BuildContext context) { + final ml10n = MaterialLocalizations.of(context); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _controller, + autofocus: true, + // usernames are all chaotic so don't let the OS make + // it harder on the user + autocorrect: false, + enableSuggestions: false, + onChanged: (value) => setState(() {}), + onSubmitted: (value) => _onSubmit(_controller.text), + decoration: InputDecoration( + filled: true, + hintText: widget.username ?? context.l10n.enterUsernameHintText, + fillColor: context.theme.colorScheme.onSecondary, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + ), + ), + Gaps.gap12, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: _onClear, + style: OutlinedButton.styleFrom( + foregroundColor: context.theme.colorScheme.error, + side: BorderSide(color: context.theme.colorScheme.error), + ), + child: Text(context.l10n.clearUsernameButtonLabel), + ), + const Spacer(), + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancelText), + ), + FilledButton( + onPressed: _isValid ? _onSubmit : null, + child: Text(ml10n.saveButtonLabel), + ), + ], + ), + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/settings/widgets/widgets.dart b/lib/settings/widgets/widgets.dart index 6ddf948d3..f8c7b2837 100644 --- a/lib/settings/widgets/widgets.dart +++ b/lib/settings/widgets/widgets.dart @@ -2,3 +2,4 @@ export 'about_app.dart'; export 'language_picker.dart'; export 'notifications.dart'; export 'theme_picker.dart'; +export 'username_input.dart'; diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/bug_report.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 50a4c7b8b..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -title: "fix: " -labels: bug ---- - -**Description** - -A clear and concise description of what the bug is. - -**Steps To Reproduce** - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected Behavior** - -A clear and concise description of what you expected to happen. - -**Screenshots** - -If applicable, add screenshots to help explain your problem. - -**Additional Context** - -Add any other context about the problem here. diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/build.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/build.md deleted file mode 100644 index 0cf8e62cd..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/build.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Build System -about: Changes that affect the build system or external dependencies -title: "build: " -labels: build ---- - -**Description** - -Describe what changes need to be done to the build system and why. - -**Requirements** - -- [ ] The build system is passing diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/chore.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/chore.md deleted file mode 100644 index 498ebfd82..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/chore.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Chore -about: Other changes that don't modify src or test files -title: "chore: " -labels: chore ---- - -**Description** - -Clearly describe what change is needed and why. If this changes code then please use another issue type. - -**Requirements** - -- [ ] No functional changes to the code diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/ci.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/ci.md deleted file mode 100644 index fa2dd9e2d..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/ci.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Continuous Integration -about: Changes to the CI configuration files and scripts -title: "ci: " -labels: ci ---- - -**Description** - -Describe what changes need to be done to the ci/cd system and why. - -**Requirements** - -- [ ] The ci system is passing diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/config.yml b/packages/fish_repository/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index ec4bb386b..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false \ No newline at end of file diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/documentation.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index f494a4d98..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Documentation -about: Improve the documentation so all collaborators have a common understanding -title: "docs: " -labels: documentation ---- - -**Description** - -Clearly describe what documentation you are looking to add or improve. - -**Requirements** - -- [ ] Requirements go here diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/feature_request.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ddd2fcca9..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature Request -about: A new feature to be added to the project -title: "feat: " -labels: feature ---- - -**Description** - -Clearly describe what you are looking to add. The more context the better. - -**Requirements** - -- [ ] Checklist of requirements to be fulfilled - -**Additional Context** - -Add any other context or screenshots about the feature request go here. diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/performance.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/performance.md deleted file mode 100644 index 699b8d45f..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/performance.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Performance Update -about: A code change that improves performance -title: "perf: " -labels: performance ---- - -**Description** - -Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/refactor.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/refactor.md deleted file mode 100644 index 1626c5704..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/refactor.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Refactor -about: A code change that neither fixes a bug nor adds a feature -title: "refactor: " -labels: refactor ---- - -**Description** - -Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/revert.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/revert.md deleted file mode 100644 index 9d121dc56..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/revert.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Revert Commit -about: Reverts a previous commit -title: "revert: " -labels: revert ---- - -**Description** - -Provide a link to a PR/Commit that you are looking to revert and why. - -**Requirements** - -- [ ] Change has been reverted -- [ ] No change in test coverage has happened -- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/style.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/style.md deleted file mode 100644 index 02244a7bd..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/style.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Style Changes -about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) -title: "style: " -labels: style ---- - -**Description** - -Clearly describe what you are looking to change and why. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/packages/fish_repository/.github/ISSUE_TEMPLATE/test.md b/packages/fish_repository/.github/ISSUE_TEMPLATE/test.md deleted file mode 100644 index 431a7ea76..000000000 --- a/packages/fish_repository/.github/ISSUE_TEMPLATE/test.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Test -about: Adding missing tests or correcting existing tests -title: "test: " -labels: test ---- - -**Description** - -List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/packages/fish_repository/.github/PULL_REQUEST_TEMPLATE.md b/packages/fish_repository/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 116993637..000000000 --- a/packages/fish_repository/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ - - -## Status - -**READY/IN DEVELOPMENT/HOLD** - -## Description - - - -## Type of Change - - - -- [ ] โœจ New feature (non-breaking change which adds functionality) -- [ ] ๐Ÿ› ๏ธ Bug fix (non-breaking change which fixes an issue) -- [ ] โŒ Breaking change (fix or feature that would cause existing functionality to change) -- [ ] ๐Ÿงน Code refactor -- [ ] โœ… Build configuration change -- [ ] ๐Ÿ“ Documentation -- [ ] ๐Ÿ—‘๏ธ Chore diff --git a/packages/fish_repository/.github/cspell.json b/packages/fish_repository/.github/cspell.json deleted file mode 100644 index 14007aa0b..000000000 --- a/packages/fish_repository/.github/cspell.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "0.2", - "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "dictionaries": ["vgv_allowed", "vgv_forbidden"], - "dictionaryDefinitions": [ - { - "name": "vgv_allowed", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", - "description": "Allowed VGV Spellings" - }, - { - "name": "vgv_forbidden", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", - "description": "Forbidden VGV Spellings" - } - ], - "useGitignore": true, - "words": [ - "fish_repository" - ] -} diff --git a/packages/fish_repository/.github/dependabot.yaml b/packages/fish_repository/.github/dependabot.yaml deleted file mode 100644 index 63b035cde..000000000 --- a/packages/fish_repository/.github/dependabot.yaml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -enable-beta-ecosystems: true -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "pub" - directory: "/" - schedule: - interval: "daily" diff --git a/packages/fish_repository/.github/workflows/main.yaml b/packages/fish_repository/.github/workflows/main.yaml deleted file mode 100644 index cf84703df..000000000 --- a/packages/fish_repository/.github/workflows/main.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: ci - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - pull_request: - branches: - - main - -jobs: - semantic_pull_request: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 - - spell-check: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 - with: - includes: "**/*.md" - modified_files_only: false - - build: - uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 - with: - flutter_channel: stable diff --git a/packages/navis_ui/pubspec.lock b/packages/navis_ui/pubspec.lock index 118404d90..0709f1df9 100644 --- a/packages/navis_ui/pubspec.lock +++ b/packages/navis_ui/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" crypto: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" ffi: dependency: transitive description: @@ -179,6 +179,14 @@ packages: description: flutter source: sdk version: "0.0.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" html: dependency: "direct main" description: @@ -228,7 +236,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: transitive + dependency: "direct overridden" description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 @@ -327,7 +335,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -436,10 +444,10 @@ packages: dependency: "direct main" description: name: warframestat_client - sha256: c270166ec3184145498a8789bc9163a83193bbaeca97653fdaa243110960f69b + sha256: d7de91878b1165d5438605b6c9fcc7fe53e08c29bfb31f3fe1a9a7015cf4dba6 url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.15.1" web: dependency: "direct overridden" description: @@ -448,22 +456,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.1" web_socket_client: dependency: transitive description: name: web_socket_client - sha256: "64c703569a5a274b4e6303de4797264051b042821b44ff516ccef354ba8b108a" + sha256: "0ec5230852349191188c013112e4d2be03e3fc83dbe80139ead9bf3a136e53b5" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.5" win32: dependency: transitive description: @@ -481,5 +497,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.16.0" diff --git a/packages/warframestat_repository/lib/src/arsenal_database.dart b/packages/warframestat_repository/lib/src/arsenal_database.dart new file mode 100644 index 000000000..77d3f32f9 --- /dev/null +++ b/packages/warframestat_repository/lib/src/arsenal_database.dart @@ -0,0 +1,88 @@ +import 'package:drift/drift.dart'; +import 'package:warframestat_client/warframestat_client.dart' + show ItemType, MinimalItem, XpItem; +import 'package:warframestat_repository/src/converters/converters.dart'; +import 'package:warframestat_repository/src/tables/tables.dart'; +import 'package:warframestat_repository/src/utils/utils.dart'; + +part 'arsenal_database.g.dart'; + +@DriftDatabase(tables: [ArsenalItem, ArsenalManifest, ArsenalItemXp]) +class ArsenalDatabase extends _$ArsenalDatabase { + ArsenalDatabase(super.e); + + @override + int get schemaVersion => 1; + + Future lastUpdate() async { + return (await select(arsenalManifest).getSingleOrNull())?.lastUpdate; + } + + Future updateTimeStamp() { + return update(arsenalManifest).write( + ArsenalManifestCompanion.insert( + id: const Value(1), + lastUpdate: DateTime.timestamp(), + ), + ); + } + + Future updateItems(List items) { + return transaction(() async { + for (final item in items) { + // Sometimes items are fixed upstream, if it's no longer masterable + // then remove it from the manifest + if (item.masterable != true) { + await (delete(arsenalItem) + ..where((i) => i.uniqueName.equals(item.uniqueName))) + .go(); + + continue; + } + + await into(arsenalItem).insertOnConflictUpdate(item.toInsertable()); + } + + await into(arsenalManifest).insertOnConflictUpdate( + ArsenalManifestCompanion.insert( + id: const Value(1), + lastUpdate: DateTime.timestamp(), + ), + ); + }); + } + + Future updateXp(List items) async { + return transaction(() async { + for (final item in items) { + await into(arsenalItemXp).insertOnConflictUpdate( + ArsenalItemXpCompanion.insert( + uniqueName: item.uniqueName, + xp: item.xp, + ), + ); + } + }); + } + + Future> fetchArsenal() { + return transaction(() async { + final items = await select(arsenalItem).get(); + final primaryKeys = items.map((i) => i.uniqueName); + final xp = await (select(arsenalItemXp) + ..where((xp) => xp.uniqueName.isIn(primaryKeys))) + .get(); + + final xpMap = {for (final x in xp) x.uniqueName: x.xp}; + + return items.map((item) { + final xp = xpMap[item.uniqueName] ?? 0; + return MasteryProgress( + item: item, + xp: xp, + missing: !xpMap.containsKey(item.uniqueName), + ); + }).toList(); + }); + } +} diff --git a/packages/warframestat_repository/lib/src/arsenal_database.g.dart b/packages/warframestat_repository/lib/src/arsenal_database.g.dart new file mode 100644 index 000000000..a2c307d73 --- /dev/null +++ b/packages/warframestat_repository/lib/src/arsenal_database.g.dart @@ -0,0 +1,1316 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'arsenal_database.dart'; + +// ignore_for_file: type=lint +class $ArsenalItemTable extends ArsenalItem + with TableInfo<$ArsenalItemTable, MinimalItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ArsenalItemTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _uniqueNameMeta = + const VerificationMeta('uniqueName'); + @override + late final GeneratedColumn uniqueName = GeneratedColumn( + 'unique_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _imageNameMeta = + const VerificationMeta('imageName'); + @override + late final GeneratedColumn imageName = GeneratedColumn( + 'image_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumnWithTypeConverter type = + GeneratedColumn('type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter($ArsenalItemTable.$convertertype); + static const VerificationMeta _productCategoryMeta = + const VerificationMeta('productCategory'); + @override + late final GeneratedColumn productCategory = GeneratedColumn( + 'product_category', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _categoryMeta = + const VerificationMeta('category'); + @override + late final GeneratedColumn category = GeneratedColumn( + 'category', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _tradableMeta = + const VerificationMeta('tradable'); + @override + late final GeneratedColumn tradable = GeneratedColumn( + 'tradable', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("tradable" IN (0, 1))')); + static const VerificationMeta _excludeFromCodexMeta = + const VerificationMeta('excludeFromCodex'); + @override + late final GeneratedColumn excludeFromCodex = GeneratedColumn( + 'exclude_from_codex', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("exclude_from_codex" IN (0, 1))')); + static const VerificationMeta _vaultDateMeta = + const VerificationMeta('vaultDate'); + @override + late final GeneratedColumn vaultDate = GeneratedColumn( + 'vault_date', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _vaultedMeta = + const VerificationMeta('vaulted'); + @override + late final GeneratedColumn vaulted = GeneratedColumn( + 'vaulted', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("vaulted" IN (0, 1))')); + static const VerificationMeta _wikiaUrlMeta = + const VerificationMeta('wikiaUrl'); + @override + late final GeneratedColumn wikiaUrl = GeneratedColumn( + 'wikia_url', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _masterableMeta = + const VerificationMeta('masterable'); + @override + late final GeneratedColumn masterable = GeneratedColumn( + 'masterable', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("masterable" IN (0, 1))')); + @override + List get $columns => [ + uniqueName, + name, + description, + imageName, + type, + productCategory, + category, + tradable, + excludeFromCodex, + vaultDate, + vaulted, + wikiaUrl, + masterable + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'arsenal_item'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('unique_name')) { + context.handle( + _uniqueNameMeta, + uniqueName.isAcceptableOrUnknown( + data['unique_name']!, _uniqueNameMeta)); + } else if (isInserting) { + context.missing(_uniqueNameMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('image_name')) { + context.handle(_imageNameMeta, + imageName.isAcceptableOrUnknown(data['image_name']!, _imageNameMeta)); + } + context.handle(_typeMeta, const VerificationResult.success()); + if (data.containsKey('product_category')) { + context.handle( + _productCategoryMeta, + productCategory.isAcceptableOrUnknown( + data['product_category']!, _productCategoryMeta)); + } + if (data.containsKey('category')) { + context.handle(_categoryMeta, + category.isAcceptableOrUnknown(data['category']!, _categoryMeta)); + } else if (isInserting) { + context.missing(_categoryMeta); + } + if (data.containsKey('tradable')) { + context.handle(_tradableMeta, + tradable.isAcceptableOrUnknown(data['tradable']!, _tradableMeta)); + } else if (isInserting) { + context.missing(_tradableMeta); + } + if (data.containsKey('exclude_from_codex')) { + context.handle( + _excludeFromCodexMeta, + excludeFromCodex.isAcceptableOrUnknown( + data['exclude_from_codex']!, _excludeFromCodexMeta)); + } + if (data.containsKey('vault_date')) { + context.handle(_vaultDateMeta, + vaultDate.isAcceptableOrUnknown(data['vault_date']!, _vaultDateMeta)); + } + if (data.containsKey('vaulted')) { + context.handle(_vaultedMeta, + vaulted.isAcceptableOrUnknown(data['vaulted']!, _vaultedMeta)); + } + if (data.containsKey('wikia_url')) { + context.handle(_wikiaUrlMeta, + wikiaUrl.isAcceptableOrUnknown(data['wikia_url']!, _wikiaUrlMeta)); + } + if (data.containsKey('masterable')) { + context.handle( + _masterableMeta, + masterable.isAcceptableOrUnknown( + data['masterable']!, _masterableMeta)); + } + return context; + } + + @override + Set get $primaryKey => {uniqueName}; + @override + MinimalItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MinimalItem( + uniqueName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}unique_name'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + type: $ArsenalItemTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!), + category: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}category'])!, + productCategory: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}product_category']), + imageName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}image_name']), + tradable: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}tradable'])!, + excludeFromCodex: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}exclude_from_codex']), + wikiaUrl: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}wikia_url']), + vaultDate: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}vault_date']), + vaulted: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}vaulted']), + masterable: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}masterable']), + ); + } + + @override + $ArsenalItemTable createAlias(String alias) { + return $ArsenalItemTable(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertertype = + const ItemTypeConverter(); +} + +class ArsenalItemCompanion extends UpdateCompanion { + final Value uniqueName; + final Value name; + final Value description; + final Value imageName; + final Value type; + final Value productCategory; + final Value category; + final Value tradable; + final Value excludeFromCodex; + final Value vaultDate; + final Value vaulted; + final Value wikiaUrl; + final Value masterable; + final Value rowid; + const ArsenalItemCompanion({ + this.uniqueName = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.imageName = const Value.absent(), + this.type = const Value.absent(), + this.productCategory = const Value.absent(), + this.category = const Value.absent(), + this.tradable = const Value.absent(), + this.excludeFromCodex = const Value.absent(), + this.vaultDate = const Value.absent(), + this.vaulted = const Value.absent(), + this.wikiaUrl = const Value.absent(), + this.masterable = const Value.absent(), + this.rowid = const Value.absent(), + }); + ArsenalItemCompanion.insert({ + required String uniqueName, + required String name, + this.description = const Value.absent(), + this.imageName = const Value.absent(), + required ItemType type, + this.productCategory = const Value.absent(), + required String category, + required bool tradable, + this.excludeFromCodex = const Value.absent(), + this.vaultDate = const Value.absent(), + this.vaulted = const Value.absent(), + this.wikiaUrl = const Value.absent(), + this.masterable = const Value.absent(), + this.rowid = const Value.absent(), + }) : uniqueName = Value(uniqueName), + name = Value(name), + type = Value(type), + category = Value(category), + tradable = Value(tradable); + static Insertable custom({ + Expression? uniqueName, + Expression? name, + Expression? description, + Expression? imageName, + Expression? type, + Expression? productCategory, + Expression? category, + Expression? tradable, + Expression? excludeFromCodex, + Expression? vaultDate, + Expression? vaulted, + Expression? wikiaUrl, + Expression? masterable, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (uniqueName != null) 'unique_name': uniqueName, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (imageName != null) 'image_name': imageName, + if (type != null) 'type': type, + if (productCategory != null) 'product_category': productCategory, + if (category != null) 'category': category, + if (tradable != null) 'tradable': tradable, + if (excludeFromCodex != null) 'exclude_from_codex': excludeFromCodex, + if (vaultDate != null) 'vault_date': vaultDate, + if (vaulted != null) 'vaulted': vaulted, + if (wikiaUrl != null) 'wikia_url': wikiaUrl, + if (masterable != null) 'masterable': masterable, + if (rowid != null) 'rowid': rowid, + }); + } + + ArsenalItemCompanion copyWith( + {Value? uniqueName, + Value? name, + Value? description, + Value? imageName, + Value? type, + Value? productCategory, + Value? category, + Value? tradable, + Value? excludeFromCodex, + Value? vaultDate, + Value? vaulted, + Value? wikiaUrl, + Value? masterable, + Value? rowid}) { + return ArsenalItemCompanion( + uniqueName: uniqueName ?? this.uniqueName, + name: name ?? this.name, + description: description ?? this.description, + imageName: imageName ?? this.imageName, + type: type ?? this.type, + productCategory: productCategory ?? this.productCategory, + category: category ?? this.category, + tradable: tradable ?? this.tradable, + excludeFromCodex: excludeFromCodex ?? this.excludeFromCodex, + vaultDate: vaultDate ?? this.vaultDate, + vaulted: vaulted ?? this.vaulted, + wikiaUrl: wikiaUrl ?? this.wikiaUrl, + masterable: masterable ?? this.masterable, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (uniqueName.present) { + map['unique_name'] = Variable(uniqueName.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (imageName.present) { + map['image_name'] = Variable(imageName.value); + } + if (type.present) { + map['type'] = + Variable($ArsenalItemTable.$convertertype.toSql(type.value)); + } + if (productCategory.present) { + map['product_category'] = Variable(productCategory.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + if (tradable.present) { + map['tradable'] = Variable(tradable.value); + } + if (excludeFromCodex.present) { + map['exclude_from_codex'] = Variable(excludeFromCodex.value); + } + if (vaultDate.present) { + map['vault_date'] = Variable(vaultDate.value); + } + if (vaulted.present) { + map['vaulted'] = Variable(vaulted.value); + } + if (wikiaUrl.present) { + map['wikia_url'] = Variable(wikiaUrl.value); + } + if (masterable.present) { + map['masterable'] = Variable(masterable.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ArsenalItemCompanion(') + ..write('uniqueName: $uniqueName, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('imageName: $imageName, ') + ..write('type: $type, ') + ..write('productCategory: $productCategory, ') + ..write('category: $category, ') + ..write('tradable: $tradable, ') + ..write('excludeFromCodex: $excludeFromCodex, ') + ..write('vaultDate: $vaultDate, ') + ..write('vaulted: $vaulted, ') + ..write('wikiaUrl: $wikiaUrl, ') + ..write('masterable: $masterable, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $ArsenalManifestTable extends ArsenalManifest + with TableInfo<$ArsenalManifestTable, ArsenalManifestData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ArsenalManifestTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _lastUpdateMeta = + const VerificationMeta('lastUpdate'); + @override + late final GeneratedColumn lastUpdate = GeneratedColumn( + 'last_update', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + @override + List get $columns => [id, lastUpdate]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'arsenal_manifest'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('last_update')) { + context.handle( + _lastUpdateMeta, + lastUpdate.isAcceptableOrUnknown( + data['last_update']!, _lastUpdateMeta)); + } else if (isInserting) { + context.missing(_lastUpdateMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ArsenalManifestData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ArsenalManifestData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + lastUpdate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_update'])!, + ); + } + + @override + $ArsenalManifestTable createAlias(String alias) { + return $ArsenalManifestTable(attachedDatabase, alias); + } +} + +class ArsenalManifestData extends DataClass + implements Insertable { + final int id; + final DateTime lastUpdate; + const ArsenalManifestData({required this.id, required this.lastUpdate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['last_update'] = Variable(lastUpdate); + return map; + } + + ArsenalManifestCompanion toCompanion(bool nullToAbsent) { + return ArsenalManifestCompanion( + id: Value(id), + lastUpdate: Value(lastUpdate), + ); + } + + factory ArsenalManifestData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ArsenalManifestData( + id: serializer.fromJson(json['id']), + lastUpdate: serializer.fromJson(json['lastUpdate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'lastUpdate': serializer.toJson(lastUpdate), + }; + } + + ArsenalManifestData copyWith({int? id, DateTime? lastUpdate}) => + ArsenalManifestData( + id: id ?? this.id, + lastUpdate: lastUpdate ?? this.lastUpdate, + ); + ArsenalManifestData copyWithCompanion(ArsenalManifestCompanion data) { + return ArsenalManifestData( + id: data.id.present ? data.id.value : this.id, + lastUpdate: + data.lastUpdate.present ? data.lastUpdate.value : this.lastUpdate, + ); + } + + @override + String toString() { + return (StringBuffer('ArsenalManifestData(') + ..write('id: $id, ') + ..write('lastUpdate: $lastUpdate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, lastUpdate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ArsenalManifestData && + other.id == this.id && + other.lastUpdate == this.lastUpdate); +} + +class ArsenalManifestCompanion extends UpdateCompanion { + final Value id; + final Value lastUpdate; + const ArsenalManifestCompanion({ + this.id = const Value.absent(), + this.lastUpdate = const Value.absent(), + }); + ArsenalManifestCompanion.insert({ + this.id = const Value.absent(), + required DateTime lastUpdate, + }) : lastUpdate = Value(lastUpdate); + static Insertable custom({ + Expression? id, + Expression? lastUpdate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (lastUpdate != null) 'last_update': lastUpdate, + }); + } + + ArsenalManifestCompanion copyWith( + {Value? id, Value? lastUpdate}) { + return ArsenalManifestCompanion( + id: id ?? this.id, + lastUpdate: lastUpdate ?? this.lastUpdate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (lastUpdate.present) { + map['last_update'] = Variable(lastUpdate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ArsenalManifestCompanion(') + ..write('id: $id, ') + ..write('lastUpdate: $lastUpdate') + ..write(')')) + .toString(); + } +} + +class $ArsenalItemXpTable extends ArsenalItemXp + with TableInfo<$ArsenalItemXpTable, XpItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ArsenalItemXpTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _uniqueNameMeta = + const VerificationMeta('uniqueName'); + @override + late final GeneratedColumn uniqueName = GeneratedColumn( + 'unique_name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _xpMeta = const VerificationMeta('xp'); + @override + late final GeneratedColumn xp = GeneratedColumn( + 'xp', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + @override + List get $columns => [uniqueName, xp]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'arsenal_item_xp'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('unique_name')) { + context.handle( + _uniqueNameMeta, + uniqueName.isAcceptableOrUnknown( + data['unique_name']!, _uniqueNameMeta)); + } else if (isInserting) { + context.missing(_uniqueNameMeta); + } + if (data.containsKey('xp')) { + context.handle(_xpMeta, xp.isAcceptableOrUnknown(data['xp']!, _xpMeta)); + } else if (isInserting) { + context.missing(_xpMeta); + } + return context; + } + + @override + Set get $primaryKey => {uniqueName}; + @override + XpItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return XpItem( + uniqueName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}unique_name'])!, + xp: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}xp'])!, + ); + } + + @override + $ArsenalItemXpTable createAlias(String alias) { + return $ArsenalItemXpTable(attachedDatabase, alias); + } +} + +class ArsenalItemXpCompanion extends UpdateCompanion { + final Value uniqueName; + final Value xp; + final Value rowid; + const ArsenalItemXpCompanion({ + this.uniqueName = const Value.absent(), + this.xp = const Value.absent(), + this.rowid = const Value.absent(), + }); + ArsenalItemXpCompanion.insert({ + required String uniqueName, + required int xp, + this.rowid = const Value.absent(), + }) : uniqueName = Value(uniqueName), + xp = Value(xp); + static Insertable custom({ + Expression? uniqueName, + Expression? xp, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (uniqueName != null) 'unique_name': uniqueName, + if (xp != null) 'xp': xp, + if (rowid != null) 'rowid': rowid, + }); + } + + ArsenalItemXpCompanion copyWith( + {Value? uniqueName, Value? xp, Value? rowid}) { + return ArsenalItemXpCompanion( + uniqueName: uniqueName ?? this.uniqueName, + xp: xp ?? this.xp, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (uniqueName.present) { + map['unique_name'] = Variable(uniqueName.value); + } + if (xp.present) { + map['xp'] = Variable(xp.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ArsenalItemXpCompanion(') + ..write('uniqueName: $uniqueName, ') + ..write('xp: $xp, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$ArsenalDatabase extends GeneratedDatabase { + _$ArsenalDatabase(QueryExecutor e) : super(e); + $ArsenalDatabaseManager get managers => $ArsenalDatabaseManager(this); + late final $ArsenalItemTable arsenalItem = $ArsenalItemTable(this); + late final $ArsenalManifestTable arsenalManifest = + $ArsenalManifestTable(this); + late final $ArsenalItemXpTable arsenalItemXp = $ArsenalItemXpTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [arsenalItem, arsenalManifest, arsenalItemXp]; +} + +typedef $$ArsenalItemTableCreateCompanionBuilder = ArsenalItemCompanion + Function({ + required String uniqueName, + required String name, + Value description, + Value imageName, + required ItemType type, + Value productCategory, + required String category, + required bool tradable, + Value excludeFromCodex, + Value vaultDate, + Value vaulted, + Value wikiaUrl, + Value masterable, + Value rowid, +}); +typedef $$ArsenalItemTableUpdateCompanionBuilder = ArsenalItemCompanion + Function({ + Value uniqueName, + Value name, + Value description, + Value imageName, + Value type, + Value productCategory, + Value category, + Value tradable, + Value excludeFromCodex, + Value vaultDate, + Value vaulted, + Value wikiaUrl, + Value masterable, + Value rowid, +}); + +class $$ArsenalItemTableFilterComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemTable> { + $$ArsenalItemTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column)); + + ColumnFilters get imageName => $composableBuilder( + column: $table.imageName, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get productCategory => $composableBuilder( + column: $table.productCategory, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnFilters(column)); + + ColumnFilters get tradable => $composableBuilder( + column: $table.tradable, builder: (column) => ColumnFilters(column)); + + ColumnFilters get excludeFromCodex => $composableBuilder( + column: $table.excludeFromCodex, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get vaultDate => $composableBuilder( + column: $table.vaultDate, builder: (column) => ColumnFilters(column)); + + ColumnFilters get vaulted => $composableBuilder( + column: $table.vaulted, builder: (column) => ColumnFilters(column)); + + ColumnFilters get wikiaUrl => $composableBuilder( + column: $table.wikiaUrl, builder: (column) => ColumnFilters(column)); + + ColumnFilters get masterable => $composableBuilder( + column: $table.masterable, builder: (column) => ColumnFilters(column)); +} + +class $$ArsenalItemTableOrderingComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemTable> { + $$ArsenalItemTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get imageName => $composableBuilder( + column: $table.imageName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get productCategory => $composableBuilder( + column: $table.productCategory, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get tradable => $composableBuilder( + column: $table.tradable, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get excludeFromCodex => $composableBuilder( + column: $table.excludeFromCodex, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get vaultDate => $composableBuilder( + column: $table.vaultDate, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get vaulted => $composableBuilder( + column: $table.vaulted, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get wikiaUrl => $composableBuilder( + column: $table.wikiaUrl, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get masterable => $composableBuilder( + column: $table.masterable, builder: (column) => ColumnOrderings(column)); +} + +class $$ArsenalItemTableAnnotationComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemTable> { + $$ArsenalItemTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + GeneratedColumn get imageName => + $composableBuilder(column: $table.imageName, builder: (column) => column); + + GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get productCategory => $composableBuilder( + column: $table.productCategory, builder: (column) => column); + + GeneratedColumn get category => + $composableBuilder(column: $table.category, builder: (column) => column); + + GeneratedColumn get tradable => + $composableBuilder(column: $table.tradable, builder: (column) => column); + + GeneratedColumn get excludeFromCodex => $composableBuilder( + column: $table.excludeFromCodex, builder: (column) => column); + + GeneratedColumn get vaultDate => + $composableBuilder(column: $table.vaultDate, builder: (column) => column); + + GeneratedColumn get vaulted => + $composableBuilder(column: $table.vaulted, builder: (column) => column); + + GeneratedColumn get wikiaUrl => + $composableBuilder(column: $table.wikiaUrl, builder: (column) => column); + + GeneratedColumn get masterable => $composableBuilder( + column: $table.masterable, builder: (column) => column); +} + +class $$ArsenalItemTableTableManager extends RootTableManager< + _$ArsenalDatabase, + $ArsenalItemTable, + MinimalItem, + $$ArsenalItemTableFilterComposer, + $$ArsenalItemTableOrderingComposer, + $$ArsenalItemTableAnnotationComposer, + $$ArsenalItemTableCreateCompanionBuilder, + $$ArsenalItemTableUpdateCompanionBuilder, + ( + MinimalItem, + BaseReferences<_$ArsenalDatabase, $ArsenalItemTable, MinimalItem> + ), + MinimalItem, + PrefetchHooks Function()> { + $$ArsenalItemTableTableManager(_$ArsenalDatabase db, $ArsenalItemTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ArsenalItemTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ArsenalItemTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ArsenalItemTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value uniqueName = const Value.absent(), + Value name = const Value.absent(), + Value description = const Value.absent(), + Value imageName = const Value.absent(), + Value type = const Value.absent(), + Value productCategory = const Value.absent(), + Value category = const Value.absent(), + Value tradable = const Value.absent(), + Value excludeFromCodex = const Value.absent(), + Value vaultDate = const Value.absent(), + Value vaulted = const Value.absent(), + Value wikiaUrl = const Value.absent(), + Value masterable = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ArsenalItemCompanion( + uniqueName: uniqueName, + name: name, + description: description, + imageName: imageName, + type: type, + productCategory: productCategory, + category: category, + tradable: tradable, + excludeFromCodex: excludeFromCodex, + vaultDate: vaultDate, + vaulted: vaulted, + wikiaUrl: wikiaUrl, + masterable: masterable, + rowid: rowid, + ), + createCompanionCallback: ({ + required String uniqueName, + required String name, + Value description = const Value.absent(), + Value imageName = const Value.absent(), + required ItemType type, + Value productCategory = const Value.absent(), + required String category, + required bool tradable, + Value excludeFromCodex = const Value.absent(), + Value vaultDate = const Value.absent(), + Value vaulted = const Value.absent(), + Value wikiaUrl = const Value.absent(), + Value masterable = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ArsenalItemCompanion.insert( + uniqueName: uniqueName, + name: name, + description: description, + imageName: imageName, + type: type, + productCategory: productCategory, + category: category, + tradable: tradable, + excludeFromCodex: excludeFromCodex, + vaultDate: vaultDate, + vaulted: vaulted, + wikiaUrl: wikiaUrl, + masterable: masterable, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ArsenalItemTableProcessedTableManager = ProcessedTableManager< + _$ArsenalDatabase, + $ArsenalItemTable, + MinimalItem, + $$ArsenalItemTableFilterComposer, + $$ArsenalItemTableOrderingComposer, + $$ArsenalItemTableAnnotationComposer, + $$ArsenalItemTableCreateCompanionBuilder, + $$ArsenalItemTableUpdateCompanionBuilder, + ( + MinimalItem, + BaseReferences<_$ArsenalDatabase, $ArsenalItemTable, MinimalItem> + ), + MinimalItem, + PrefetchHooks Function()>; +typedef $$ArsenalManifestTableCreateCompanionBuilder = ArsenalManifestCompanion + Function({ + Value id, + required DateTime lastUpdate, +}); +typedef $$ArsenalManifestTableUpdateCompanionBuilder = ArsenalManifestCompanion + Function({ + Value id, + Value lastUpdate, +}); + +class $$ArsenalManifestTableFilterComposer + extends Composer<_$ArsenalDatabase, $ArsenalManifestTable> { + $$ArsenalManifestTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastUpdate => $composableBuilder( + column: $table.lastUpdate, builder: (column) => ColumnFilters(column)); +} + +class $$ArsenalManifestTableOrderingComposer + extends Composer<_$ArsenalDatabase, $ArsenalManifestTable> { + $$ArsenalManifestTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get lastUpdate => $composableBuilder( + column: $table.lastUpdate, builder: (column) => ColumnOrderings(column)); +} + +class $$ArsenalManifestTableAnnotationComposer + extends Composer<_$ArsenalDatabase, $ArsenalManifestTable> { + $$ArsenalManifestTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get lastUpdate => $composableBuilder( + column: $table.lastUpdate, builder: (column) => column); +} + +class $$ArsenalManifestTableTableManager extends RootTableManager< + _$ArsenalDatabase, + $ArsenalManifestTable, + ArsenalManifestData, + $$ArsenalManifestTableFilterComposer, + $$ArsenalManifestTableOrderingComposer, + $$ArsenalManifestTableAnnotationComposer, + $$ArsenalManifestTableCreateCompanionBuilder, + $$ArsenalManifestTableUpdateCompanionBuilder, + ( + ArsenalManifestData, + BaseReferences<_$ArsenalDatabase, $ArsenalManifestTable, + ArsenalManifestData> + ), + ArsenalManifestData, + PrefetchHooks Function()> { + $$ArsenalManifestTableTableManager( + _$ArsenalDatabase db, $ArsenalManifestTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ArsenalManifestTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ArsenalManifestTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ArsenalManifestTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value lastUpdate = const Value.absent(), + }) => + ArsenalManifestCompanion( + id: id, + lastUpdate: lastUpdate, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required DateTime lastUpdate, + }) => + ArsenalManifestCompanion.insert( + id: id, + lastUpdate: lastUpdate, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ArsenalManifestTableProcessedTableManager = ProcessedTableManager< + _$ArsenalDatabase, + $ArsenalManifestTable, + ArsenalManifestData, + $$ArsenalManifestTableFilterComposer, + $$ArsenalManifestTableOrderingComposer, + $$ArsenalManifestTableAnnotationComposer, + $$ArsenalManifestTableCreateCompanionBuilder, + $$ArsenalManifestTableUpdateCompanionBuilder, + ( + ArsenalManifestData, + BaseReferences<_$ArsenalDatabase, $ArsenalManifestTable, + ArsenalManifestData> + ), + ArsenalManifestData, + PrefetchHooks Function()>; +typedef $$ArsenalItemXpTableCreateCompanionBuilder = ArsenalItemXpCompanion + Function({ + required String uniqueName, + required int xp, + Value rowid, +}); +typedef $$ArsenalItemXpTableUpdateCompanionBuilder = ArsenalItemXpCompanion + Function({ + Value uniqueName, + Value xp, + Value rowid, +}); + +class $$ArsenalItemXpTableFilterComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemXpTable> { + $$ArsenalItemXpTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get xp => $composableBuilder( + column: $table.xp, builder: (column) => ColumnFilters(column)); +} + +class $$ArsenalItemXpTableOrderingComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemXpTable> { + $$ArsenalItemXpTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get xp => $composableBuilder( + column: $table.xp, builder: (column) => ColumnOrderings(column)); +} + +class $$ArsenalItemXpTableAnnotationComposer + extends Composer<_$ArsenalDatabase, $ArsenalItemXpTable> { + $$ArsenalItemXpTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get uniqueName => $composableBuilder( + column: $table.uniqueName, builder: (column) => column); + + GeneratedColumn get xp => + $composableBuilder(column: $table.xp, builder: (column) => column); +} + +class $$ArsenalItemXpTableTableManager extends RootTableManager< + _$ArsenalDatabase, + $ArsenalItemXpTable, + XpItem, + $$ArsenalItemXpTableFilterComposer, + $$ArsenalItemXpTableOrderingComposer, + $$ArsenalItemXpTableAnnotationComposer, + $$ArsenalItemXpTableCreateCompanionBuilder, + $$ArsenalItemXpTableUpdateCompanionBuilder, + (XpItem, BaseReferences<_$ArsenalDatabase, $ArsenalItemXpTable, XpItem>), + XpItem, + PrefetchHooks Function()> { + $$ArsenalItemXpTableTableManager( + _$ArsenalDatabase db, $ArsenalItemXpTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ArsenalItemXpTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ArsenalItemXpTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ArsenalItemXpTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value uniqueName = const Value.absent(), + Value xp = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ArsenalItemXpCompanion( + uniqueName: uniqueName, + xp: xp, + rowid: rowid, + ), + createCompanionCallback: ({ + required String uniqueName, + required int xp, + Value rowid = const Value.absent(), + }) => + ArsenalItemXpCompanion.insert( + uniqueName: uniqueName, + xp: xp, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$ArsenalItemXpTableProcessedTableManager = ProcessedTableManager< + _$ArsenalDatabase, + $ArsenalItemXpTable, + XpItem, + $$ArsenalItemXpTableFilterComposer, + $$ArsenalItemXpTableOrderingComposer, + $$ArsenalItemXpTableAnnotationComposer, + $$ArsenalItemXpTableCreateCompanionBuilder, + $$ArsenalItemXpTableUpdateCompanionBuilder, + (XpItem, BaseReferences<_$ArsenalDatabase, $ArsenalItemXpTable, XpItem>), + XpItem, + PrefetchHooks Function()>; + +class $ArsenalDatabaseManager { + final _$ArsenalDatabase _db; + $ArsenalDatabaseManager(this._db); + $$ArsenalItemTableTableManager get arsenalItem => + $$ArsenalItemTableTableManager(_db, _db.arsenalItem); + $$ArsenalManifestTableTableManager get arsenalManifest => + $$ArsenalManifestTableTableManager(_db, _db.arsenalManifest); + $$ArsenalItemXpTableTableManager get arsenalItemXp => + $$ArsenalItemXpTableTableManager(_db, _db.arsenalItemXp); +} diff --git a/packages/warframestat_repository/lib/src/converters/converters.dart b/packages/warframestat_repository/lib/src/converters/converters.dart new file mode 100644 index 000000000..63616c84b --- /dev/null +++ b/packages/warframestat_repository/lib/src/converters/converters.dart @@ -0,0 +1 @@ +export 'item_type_converter.dart'; diff --git a/packages/warframestat_repository/lib/src/converters/item_type_converter.dart b/packages/warframestat_repository/lib/src/converters/item_type_converter.dart new file mode 100644 index 000000000..fb60cdea0 --- /dev/null +++ b/packages/warframestat_repository/lib/src/converters/item_type_converter.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:warframestat_client/warframestat_client.dart' + hide ItemTypeConverter; + +class ItemTypeConverter extends TypeConverter + with JsonTypeConverter { + const ItemTypeConverter(); + + @override + ItemType fromSql(String fromDb) { + return ItemType.byType(fromDb); + } + + @override + String toSql(ItemType value) { + return value.type; + } +} diff --git a/packages/warframestat_repository/lib/src/repository.dart b/packages/warframestat_repository/lib/src/repository.dart index 4897e2e8a..03ef2cf75 100644 --- a/packages/warframestat_repository/lib/src/repository.dart +++ b/packages/warframestat_repository/lib/src/repository.dart @@ -1,23 +1,32 @@ +import 'dart:developer' as developer; import 'dart:io'; import 'package:hive_ce/hive.dart'; import 'package:http/http.dart'; import 'package:warframestat_client/warframestat_client.dart'; import 'package:warframestat_repository/hive_registrar.g.dart'; +import 'package:warframestat_repository/src/arsenal_database.dart'; import 'package:warframestat_repository/src/cache_client.dart'; +import 'package:warframestat_repository/src/utils/utils.dart'; /// const userAgent = 'navis'; +const _name = 'WarframestatRepository'; /// {@template warframestat_repository} /// Entry point for Warframestatus endpoints used in Cephalon Navis /// {@endtemplate} class WarframestatRepository { /// {@macro warframestat_repository} - WarframestatRepository({Client? client}) : _client = client ?? Client() { + WarframestatRepository({ + required ArsenalDatabase database, + required Client client, + }) : _database = database, + _client = client { Hive.registerAdapters(); } + final ArsenalDatabase _database; final Client _client; /// The locale request will be made for @@ -82,7 +91,7 @@ class WarframestatRepository { } /// Get one item based on unique name - Future fetchItem(String uniqueName) async { + Future fetchItem(String uniqueName) async { const cacheTime = Duration(minutes: 5); final client = WarframeItemsClient( client: await _cacheClient(cacheTime), @@ -92,4 +101,54 @@ class WarframestatRepository { return client.fetchItem(uniqueName); } + + Future fetchProfile(String username) async { + const cacheTime = Duration(minutes: 60); + final client = ProfileClient( + username: username, + client: await _cacheClient(cacheTime), + // ua: userAgent, + language: language, + ); + + return client.fetchProfile(); + } + + Future updateArsenalItems({bool update = false}) async { + final lastUpdate = await _database.lastUpdate(); + final needsUpdate = lastUpdate == null || + lastUpdate.difference(DateTime.timestamp()) > const Duration(days: 7) || + update; + + if (!needsUpdate) return; + + final client = WarframeItemsClient( + client: _client, + ua: userAgent, + language: language, + ); + + developer.log('updating arsenal manifest', name: _name); + final items = + List.from(await client.fetchAllItems(minimal: true)) + ..removeWhere((i) => i.masterable != true); + + await _database.updateItems(items); + await _database.updateTimeStamp(); + } + + Future> syncXpInfo(String username) async { + final profileClient = ProfileClient( + username: username, + client: _client, + // ua: userAgent, + language: language, + ); + + developer.log('syncing xp info', name: _name); + final xpInfo = await profileClient.fetchXpInfo(); + await _database.updateXp(xpInfo); + + return _database.fetchArsenal(); + } } diff --git a/packages/warframestat_repository/lib/src/tables/arsenal_item.dart b/packages/warframestat_repository/lib/src/tables/arsenal_item.dart new file mode 100644 index 000000000..ac7596f11 --- /dev/null +++ b/packages/warframestat_repository/lib/src/tables/arsenal_item.dart @@ -0,0 +1,23 @@ +import 'package:drift/drift.dart'; +import 'package:warframestat_client/warframestat_client.dart' show MinimalItem; +import 'package:warframestat_repository/src/converters/converters.dart'; + +@UseRowClass(MinimalItem) +class ArsenalItem extends Table { + TextColumn get uniqueName => text()(); + TextColumn get name => text()(); + TextColumn get description => text().nullable()(); + TextColumn get imageName => text().nullable()(); + TextColumn get type => text().map(const ItemTypeConverter())(); + TextColumn get productCategory => text().nullable()(); + TextColumn get category => text()(); + BoolColumn get tradable => boolean()(); + BoolColumn get excludeFromCodex => boolean().nullable()(); + TextColumn get vaultDate => text().nullable()(); + BoolColumn get vaulted => boolean().nullable()(); + TextColumn get wikiaUrl => text().nullable()(); + BoolColumn get masterable => boolean().nullable()(); + + @override + Set>? get primaryKey => {uniqueName}; +} diff --git a/packages/warframestat_repository/lib/src/tables/arsenal_manifest.dart b/packages/warframestat_repository/lib/src/tables/arsenal_manifest.dart new file mode 100644 index 000000000..5b2ff3e48 --- /dev/null +++ b/packages/warframestat_repository/lib/src/tables/arsenal_manifest.dart @@ -0,0 +1,6 @@ +import 'package:drift/drift.dart'; + +class ArsenalManifest extends Table { + IntColumn get id => integer().autoIncrement()(); + DateTimeColumn get lastUpdate => dateTime()(); +} diff --git a/packages/warframestat_repository/lib/src/tables/arsenal_xp_item.dart b/packages/warframestat_repository/lib/src/tables/arsenal_xp_item.dart new file mode 100644 index 000000000..3302076bb --- /dev/null +++ b/packages/warframestat_repository/lib/src/tables/arsenal_xp_item.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:warframestat_client/warframestat_client.dart'; + +@UseRowClass(XpItem) +class ArsenalItemXp extends Table { + TextColumn get uniqueName => text()(); + IntColumn get xp => integer()(); + + @override + Set>? get primaryKey => {uniqueName}; +} diff --git a/packages/warframestat_repository/lib/src/tables/tables.dart b/packages/warframestat_repository/lib/src/tables/tables.dart new file mode 100644 index 000000000..21442cf2a --- /dev/null +++ b/packages/warframestat_repository/lib/src/tables/tables.dart @@ -0,0 +1,3 @@ +export 'arsenal_item.dart'; +export 'arsenal_manifest.dart'; +export 'arsenal_xp_item.dart'; diff --git a/packages/warframestat_repository/lib/src/utils/extensions.dart b/packages/warframestat_repository/lib/src/utils/extensions.dart new file mode 100644 index 000000000..e1b671472 --- /dev/null +++ b/packages/warframestat_repository/lib/src/utils/extensions.dart @@ -0,0 +1,29 @@ +import 'package:drift/drift.dart'; +import 'package:warframestat_client/warframestat_client.dart'; +import 'package:warframestat_repository/src/arsenal_database.dart'; + +extension InsertableItem on MinimalItem { + Insertable toInsertable() { + return ArsenalItemCompanion.insert( + uniqueName: uniqueName, + name: name, + description: Value(description), + imageName: Value(imageName), + type: type, + productCategory: Value(productCategory), + category: category, + tradable: tradable, + excludeFromCodex: Value(excludeFromCodex), + vaultDate: Value(vaultDate), + vaulted: Value(vaulted), + masterable: Value(masterable), + wikiaUrl: Value(wikiaUrl), + ); + } +} + +extension InsertableXpItem on XpItem { + Insertable toInsertable() { + return ArsenalItemXpCompanion.insert(uniqueName: uniqueName, xp: xp); + } +} diff --git a/packages/warframestat_repository/lib/src/utils/mastery_progress.dart b/packages/warframestat_repository/lib/src/utils/mastery_progress.dart new file mode 100644 index 000000000..7c88609cd --- /dev/null +++ b/packages/warframestat_repository/lib/src/utils/mastery_progress.dart @@ -0,0 +1,42 @@ +import 'dart:math'; + +import 'package:warframestat_client/warframestat_client.dart'; + +class MasteryProgress { + MasteryProgress({ + required this.item, + required this.xp, + required this.missing, + }); + + final MinimalItem item; + final int xp; + final bool missing; + + int get rank => min(sqrt(xp / 1000).floor(), maxRank); + + int get masteryPoints { + const basePoints = 3000; + const extra = 1000; + + final isWeapon = item.type.isWeapon; + if (maxRank > 30) { + const points = basePoints + extra; + + return isWeapon ? points : points * 2; + } + + return isWeapon ? basePoints : basePoints * 2; + } + + int get maxRank { + const maxRank = 30; + const overlevel = maxRank + 10; + + final lichWeaponsRegEx = RegExp('Kuva|Tenet|Paracesis'); + if (lichWeaponsRegEx.hasMatch(item.name)) return overlevel; + if (item.productCategory == 'MechSuits') return overlevel; + + return maxRank; + } +} diff --git a/packages/warframestat_repository/lib/src/utils/utils.dart b/packages/warframestat_repository/lib/src/utils/utils.dart new file mode 100644 index 000000000..a5bd3ce45 --- /dev/null +++ b/packages/warframestat_repository/lib/src/utils/utils.dart @@ -0,0 +1,2 @@ +export 'extensions.dart'; +export 'mastery_progress.dart'; diff --git a/packages/warframestat_repository/lib/warframestat_repository.dart b/packages/warframestat_repository/lib/warframestat_repository.dart index 6780e7315..76c26a11b 100644 --- a/packages/warframestat_repository/lib/warframestat_repository.dart +++ b/packages/warframestat_repository/lib/warframestat_repository.dart @@ -1,5 +1,7 @@ /// A Very Good Project created by Very Good CLI. library; +export 'src/arsenal_database.dart'; export 'src/cache_client.dart'; export 'src/repository.dart'; +export 'src/utils/utils.dart'; diff --git a/packages/warframestat_repository/pubspec.yaml b/packages/warframestat_repository/pubspec.yaml index d04ef4b26..bdce9d591 100644 --- a/packages/warframestat_repository/pubspec.yaml +++ b/packages/warframestat_repository/pubspec.yaml @@ -4,16 +4,18 @@ version: 0.1.0+1 publish_to: none environment: - sdk: "^3.3.0" + sdk: ^3.6.0 dependencies: collection: ^1.18.0 + drift: ^2.22.1 hive_ce: ^2.7.0+1 http: ^1.2.1 - warframestat_client: ^3.8.11 + warframestat_client: ^3.15.2 dev_dependencies: build_runner: ^2.4.13 + drift_dev: ^2.22.1 hive_ce_generator: ^1.7.0 mocktail: ^1.0.3 path: ^1.9.0 diff --git a/packages/warframestat_repository/test/warframestat_repository_test.dart b/packages/warframestat_repository/test/warframestat_repository_test.dart index e77cefe28..3eedfec19 100644 --- a/packages/warframestat_repository/test/warframestat_repository_test.dart +++ b/packages/warframestat_repository/test/warframestat_repository_test.dart @@ -14,11 +14,14 @@ class MockClient extends Mock implements Client {} class MockBaseRequest extends Mock implements BaseRequest {} +class MockArsenalDatabase extends Mock implements ArsenalDatabase {} + StreamedResponse fakeResponse(String data) => StreamedResponse(Stream.value(utf8.encode(data)), 200); void main() { late Client client; + late ArsenalDatabase database; late WarframestatRepository repository; setUpAll(() async { @@ -29,7 +32,8 @@ void main() { setUp(() async { client = MockClient(); - repository = WarframestatRepository(client: client); + database = MockArsenalDatabase(); + repository = WarframestatRepository(client: client, database: database); }); tearDown(() async { @@ -101,7 +105,7 @@ void main() { final item = await repository.fetchItem(uniqueName); - expect(item.name, name); + expect(item?.name, name); }); }); } diff --git a/pubspec.lock b/pubspec.lock index 9ec53478b..4cd2744e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -342,14 +342,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - dart_mappable: - dependency: transitive - description: - name: dart_mappable - sha256: f69a961ae8589724ebb542e588f228ae844c5f78028899cbe2cc718977c1b382 - url: "https://pub.dev" - source: hosted - version: "4.3.0" dart_style: dependency: transitive description: @@ -398,6 +390,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + drift: + dependency: transitive + description: + name: drift + sha256: c2d073d35ad441730812f4ea05b5dd031fb81c5f9786a4f5fb77ecd6307b6f74 + url: "https://pub.dev" + source: hosted + version: "2.22.1" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: "69091c499a1550a84f153bafa3423f4297d26bfef3710323b141336ffe155dff" + url: "https://pub.dev" + source: hosted + version: "0.2.2" dynamic_color: dependency: "direct main" description: @@ -1372,6 +1380,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9" + url: "https://pub.dev" + source: hosted + version: "0.5.27" stack_trace: dependency: transitive description: @@ -1468,14 +1492,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" - type_plus: - dependency: transitive - description: - name: type_plus - sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 - url: "https://pub.dev" - source: hosted - version: "2.1.1" typed_data: dependency: transitive description: @@ -1576,10 +1592,10 @@ packages: dependency: "direct main" description: name: warframestat_client - sha256: "2cdf016c7e6b98a4dd757b8d2778a6bad466e9ab7218dda9e333a33e1d70f9f7" + sha256: "7e4daf24aeda89009e5573f5a12c95d2e1d31ca9c59255fecf13437efda7e425" url: "https://pub.dev" source: hosted - version: "3.11.0" + version: "3.15.3" warframestat_repository: dependency: "direct main" description: @@ -1676,5 +1692,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9dcf9d3ff..43acddf95 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: cached_network_image: ^3.3.1 collection: ^1.18.0 connecteo: ^3.0.0 + drift_flutter: ^0.2.2 dynamic_color: ^1.7.0 equatable: ^2.0.5 feedback_sentry: ^3.1.0 @@ -45,7 +46,7 @@ dependencies: sentry_hive: ^8.10.1 sentry_logging: ^8.9.0 simple_icons: ^10.1.3 - warframestat_client: ^3.11.0 + warframestat_client: ^3.15.3 warframestat_repository: path: packages/warframestat_repository From f93aaef6a8f4c4be9ba9f4adc94c2dc8b11f3825 Mon Sep 17 00:00:00 2001 From: SlayerOrnstein <6075693+SlayerOrnstein@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:07:13 -0500 Subject: [PATCH 2/2] chore: forgot what I was doing but looks like code cleanup --- lib/app/view/app_root.dart | 35 +--------- lib/app/widgets/bloc_bootstrap.dart | 17 +++++ lib/arsenal/cubit/arsenal_cubit.dart | 36 +++++++++- lib/arsenal/widgets/arsenal_item.dart | 10 ++- lib/home/widgets/activities_section.dart | 17 ++--- lib/home/widgets/news_section.dart | 70 +++++++++++-------- lib/router/app_router.dart | 2 +- lib/settings/cubit/user_settings_cubit.dart | 2 + .../cubits/worldstate/worldstate_state.dart | 24 ++----- lib/worldstate/views/timers.dart | 15 ++-- .../widgets/timers/alerts_card.dart | 17 +++-- .../widgets/timers/arbitration_card.dart | 41 ++++++----- lib/worldstate/widgets/timers/event_card.dart | 29 ++------ lib/worldstate/widgets/timers/rewards.dart | 4 +- lib/worldstate/widgets/timers/steel_path.dart | 25 +++---- .../widgets/timers/trader_card.dart | 60 +++++++--------- .../navis_ui/lib/src/constants/constants.dart | 1 - .../lib/src/constants/default_durations.dart | 1 - packages/notification_repository/pubspec.lock | 2 +- .../lib/src/utils/extensions.dart | 17 +++++ 20 files changed, 217 insertions(+), 208 deletions(-) delete mode 100644 packages/navis_ui/lib/src/constants/default_durations.dart diff --git a/lib/app/view/app_root.dart b/lib/app/view/app_root.dart index fec2ed56d..c95d5be65 100644 --- a/lib/app/view/app_root.dart +++ b/lib/app/view/app_root.dart @@ -4,7 +4,6 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:feedback_sentry/feedback_sentry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:navis/arsenal/arsenal.dart'; import 'package:navis/l10n/l10n.dart'; import 'package:navis/router/app_router.dart'; import 'package:navis/settings/settings.dart'; @@ -77,48 +76,18 @@ class _NavisAppState extends State with WidgetsBindingObserver { Locale? locale, Iterable supportedLocales, ) { - const defaultLocale = Locale('en'); - Locale? newLocale; - + var newLocale = const Locale('en'); for (final supportedLocale in supportedLocales) { if (locale?.languageCode == supportedLocale.languageCode) { newLocale = supportedLocale; } } - newLocale ??= defaultLocale; - - final userSettingsCubit = context.read(); - final settings = userSettingsCubit.state; - final language = switch (settings) { - UserSettingsSuccess() => settings.language, - _ => defaultLocale - }; - - if (language != newLocale) { - userSettingsCubit.updateLanguage(newLocale); - } + context.read().updateLanguage(newLocale); return newLocale; } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - BlocProvider.of(context).fetchWorldstate(); - - final settings = context.read().state; - final username = switch (settings) { - UserSettingsSuccess() => settings.username, - _ => null, - }; - - if (username != null) { - BlocProvider.of(context).updateArsenal(username); - } - } - @override Widget build(BuildContext context) { final settings = context.watch().state; diff --git a/lib/app/widgets/bloc_bootstrap.dart b/lib/app/widgets/bloc_bootstrap.dart index 31dc6d07d..394cc04b3 100644 --- a/lib/app/widgets/bloc_bootstrap.dart +++ b/lib/app/widgets/bloc_bootstrap.dart @@ -31,6 +31,23 @@ class _BlocBootstrapState extends State { _arsenalCubit = ArsenalCubit(wsRepo); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _worldstateCubit.fetchWorldstate(); + + final state = _userSettingsCubit.state; + final username = switch (state) { + UserSettingsSuccess() => state.username, + _ => null, + }; + + if (username != null) { + _arsenalCubit.updateArsenal(username); + } + } + @override Widget build(BuildContext context) { return MultiBlocProvider( diff --git a/lib/arsenal/cubit/arsenal_cubit.dart b/lib/arsenal/cubit/arsenal_cubit.dart index b7170a731..916fe1d25 100644 --- a/lib/arsenal/cubit/arsenal_cubit.dart +++ b/lib/arsenal/cubit/arsenal_cubit.dart @@ -1,13 +1,14 @@ -import 'package:bloc/bloc.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:warframestat_client/warframestat_client.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; part 'arsenal_state.dart'; -class ArsenalCubit extends Cubit { +class ArsenalCubit extends HydratedCubit { ArsenalCubit(this.repository) : super(ArsenalInitial()); final WarframestatRepository repository; @@ -29,4 +30,35 @@ class ArsenalCubit extends Cubit { await Sentry.captureException(e, stackTrace: stack); } } + + @override + ArsenalState? fromJson(Map json) { + final inProgess = + List>.from(json['inProgress'] as List).map( + (m) => MasteryProgress( + item: MinimalItem.fromJson(m['item'] as Map), + xp: m['xp'] as int, + missing: m['missing'] as bool, + ), + ); + + return ArsenalSuccess(inProgess.toList()); + } + + @override + Map? toJson(ArsenalState state) { + if (state is! ArsenalSuccess) return null; + + return { + 'inProgress': state.inProgress + .map( + (p) => { + 'item': p.item.toJson(), + 'xp': p.xp, + 'missing': p.missing, + }, + ) + .toList(), + }; + } } diff --git a/lib/arsenal/widgets/arsenal_item.dart b/lib/arsenal/widgets/arsenal_item.dart index a358f6ffb..3fa72ee66 100644 --- a/lib/arsenal/widgets/arsenal_item.dart +++ b/lib/arsenal/widgets/arsenal_item.dart @@ -11,11 +11,19 @@ class ArsenalItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { + const leadingSize = 50.0; + return AppCard( child: ListTile( leading: CachedNetworkImage( imageUrl: arsenalItem.item.imageUrl, - width: 50, + width: leadingSize, + errorWidget: (context, url, error) { + return const Icon( + WarframeSymbols.menu_LotusEmblem, + size: leadingSize, + ); + }, ), title: Text(arsenalItem.item.name), subtitle: Column( diff --git a/lib/home/widgets/activities_section.dart b/lib/home/widgets/activities_section.dart index 14b46beeb..7aa1fcb84 100644 --- a/lib/home/widgets/activities_section.dart +++ b/lib/home/widgets/activities_section.dart @@ -6,6 +6,8 @@ import 'package:navis/l10n/l10n.dart'; import 'package:navis/router/routes.dart'; import 'package:navis/worldstate/worldstate.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; class ActivitiesSection extends StatelessWidget { const ActivitiesSection({super.key}); @@ -25,15 +27,14 @@ class _ActivitiesContent extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final worldstate = switch (state) { - WorldstateSuccess() => state, - _ => null, - }; - + return BlocSelector( + selector: (state) => switch (state) { + WorldstateSuccess() => state.worldstate, + _ => null, + }, + builder: (context, worldstate) { return ViewLoading( - isLoading: state is! WorldstateSuccess, + isLoading: worldstate == null, child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/home/widgets/news_section.dart b/lib/home/widgets/news_section.dart index e0ad01b33..13b3b1a90 100644 --- a/lib/home/widgets/news_section.dart +++ b/lib/home/widgets/news_section.dart @@ -8,6 +8,7 @@ import 'package:navis/l10n/l10n.dart'; import 'package:navis/router/routes.dart'; import 'package:navis/worldstate/worldstate.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; class NewsSection extends StatelessWidget { const NewsSection({super.key}); @@ -43,6 +44,15 @@ class __NewsCarouselViewState extends State<_NewsCarouselView> { void _autoScroll() { if (!mounted) return; + final news = context.select?>( + (s) => switch (s) { + WorldstateSuccess() => s.worldstate.news, + _ => null, + }, + ); + + if (news == null) return; + final pageSize = MediaQuery.sizeOf(context).width * .9; var nextPage = (_currentPage + 1) % _maxItems; if (nextPage > _maxItems) nextPage = 0; @@ -74,30 +84,33 @@ class __NewsCarouselViewState extends State<_NewsCarouselView> { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is! WorldstateSuccess) { - return const Center(child: WarframeSpinner()); - } - - final news = state.worldstate.news.take(_maxItems).toList(); - final itemExtent = MediaQuery.sizeOf(context).width * .9; - - return SizedBox( - height: 200, - child: GestureDetector( - onTapDown: (_) => _timer?.cancel(), - onHorizontalDragStart: (_) => _timer?.cancel(), - onHorizontalDragEnd: (_) { - final position = _controller.position.pixels; - _currentPage = (position / itemExtent).round(); - - _timer = Timer.periodic( - _autoScrollDuration, - (_) => _autoScroll(), - ); - }, - child: CarouselView( + final itemExtent = MediaQuery.sizeOf(context).width * .9; + + return SizedBox( + height: 200, + child: GestureDetector( + onTapDown: (_) => _timer?.cancel(), + onHorizontalDragStart: (_) => _timer?.cancel(), + onHorizontalDragEnd: (_) { + final position = _controller.position.pixels; + _currentPage = (position / itemExtent).round(); + + _timer = Timer.periodic( + _autoScrollDuration, + (_) => _autoScroll(), + ); + }, + child: BlocSelector?>( + selector: (state) { + return switch (state) { + WorldstateSuccess() => state.worldstate.news, + _ => null, + }; + }, + builder: (context, news) { + if (news == null) return const Center(child: WarframeSpinner()); + + return CarouselView( controller: _controller, itemSnapping: true, itemExtent: itemExtent, @@ -107,6 +120,7 @@ class __NewsCarouselViewState extends State<_NewsCarouselView> { borderRadius: BorderRadius.circular(6), ), children: news + .take(_maxItems) .map( (n) => AppCard( padding: EdgeInsets.zero, @@ -115,10 +129,10 @@ class __NewsCarouselViewState extends State<_NewsCarouselView> { ), ) .toList(), - ), - ), - ); - }, + ); + }, + ), + ), ); } diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index 72b1bd259..7f9e2720e 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -24,7 +24,7 @@ class AppRouter { ) { return GoRouter( navigatorKey: navigatorKey, - initialLocation: const ActivitesPageRouteData().location, + initialLocation: const OverviewPageRouteData().location, observers: [SentryNavigatorObserver()], debugLogDiagnostics: debugLogDiagnostics, routes: $appRoutes, diff --git a/lib/settings/cubit/user_settings_cubit.dart b/lib/settings/cubit/user_settings_cubit.dart index de6369c5a..0795ba41c 100644 --- a/lib/settings/cubit/user_settings_cubit.dart +++ b/lib/settings/cubit/user_settings_cubit.dart @@ -32,6 +32,8 @@ class UserSettingsCubit extends Cubit { } void updateLanguage(Locale language) { + if (_settings.language == language) return; + _settings.language = language; emit((state as UserSettingsSuccess).copyWith(language: _settings.language)); } diff --git a/lib/worldstate/cubits/worldstate/worldstate_state.dart b/lib/worldstate/cubits/worldstate/worldstate_state.dart index 0270265a7..d8f02bbff 100644 --- a/lib/worldstate/cubits/worldstate/worldstate_state.dart +++ b/lib/worldstate/cubits/worldstate/worldstate_state.dart @@ -4,37 +4,21 @@ sealed class SolsystemState extends Equatable { const SolsystemState(); } -class SolsystemInitial extends SolsystemState { +final class SolsystemInitial extends SolsystemState { @override List get props => []; } -class LoadingWorldstate extends SolsystemState { +final class LoadingWorldstate extends SolsystemState { @override List get props => []; } -class WorldstateSuccess extends SolsystemState { +final class WorldstateSuccess extends SolsystemState { const WorldstateSuccess(this.worldstate); final Worldstate worldstate; - // bool get activeAcolytes => worldstate.enemyActive; - - bool get activeAlerts => worldstate.alerts.isNotEmpty; - - bool get activeSales => worldstate.flashSales.isNotEmpty; - - bool get arbitrationActive => - worldstate.arbitration != null && - !worldstate.arbitration!.node.contains('000'); - - bool get eventsActive => worldstate.events.isNotEmpty; - - bool get outpostDetected => worldstate.sentientOutposts != null; - - bool get deepArchimedeaActive => worldstate.deepArchimedea != null; - @override List get props => [worldstate]; @@ -44,7 +28,7 @@ class WorldstateSuccess extends SolsystemState { } } -class WorldstateFailure extends SolsystemState { +final class WorldstateFailure extends SolsystemState { const WorldstateFailure(this.message); final String message; diff --git a/lib/worldstate/views/timers.dart b/lib/worldstate/views/timers.dart index 077f59d07..1a2725917 100644 --- a/lib/worldstate/views/timers.dart +++ b/lib/worldstate/views/timers.dart @@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:navis/worldstate/worldstate.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; class Timers extends StatelessWidget { const Timers({super.key}); @@ -22,13 +24,12 @@ class _MobileTimers extends StatelessWidget { Widget build(BuildContext context) { const cacheExtent = 500.0; - return BlocBuilder( - builder: (_, state) { - final worldstate = switch (state) { - WorldstateSuccess() => state, - _ => null, - }; - + return BlocSelector( + selector: (state) => switch (state) { + WorldstateSuccess() => state.worldstate, + _ => null, + }, + builder: (_, worldstate) { return ViewLoading( isLoading: worldstate == null, child: ListView( diff --git a/lib/worldstate/widgets/timers/alerts_card.dart b/lib/worldstate/widgets/timers/alerts_card.dart index 4a2bff398..e370a9ef6 100644 --- a/lib/worldstate/widgets/timers/alerts_card.dart +++ b/lib/worldstate/widgets/timers/alerts_card.dart @@ -13,7 +13,7 @@ import 'package:warframestat_repository/warframestat_repository.dart'; class AlertsCard extends StatelessWidget { const AlertsCard({super.key}); - Widget _buildAlerts(Alert a, WarframestatRepository r) { + Widget _buildAlert(Alert a, WarframestatRepository r) { final reward = a.mission.reward!.items.firstOrNull?.replaceAll('Blueprint', '').trim(); @@ -29,16 +29,15 @@ class AlertsCard extends StatelessWidget { Widget build(BuildContext context) { final wsRepo = RepositoryProvider.of(context); - return BlocBuilder( - builder: (context, state) { - final alerts = switch (state) { - WorldstateSuccess() => state.worldstate.alerts, - _ => [], - }; - + return BlocSelector>( + selector: (s) => switch (s) { + WorldstateSuccess() => s.worldstate.alerts, + _ => [], + }, + builder: (context, alerts) { return Column( children: alerts - .map((a) => _buildAlerts(a, wsRepo)) + .map((a) => _buildAlert(a, wsRepo)) .map((i) => AppCard(child: i)) .toList(), ); diff --git a/lib/worldstate/widgets/timers/arbitration_card.dart b/lib/worldstate/widgets/timers/arbitration_card.dart index 8aa5c9f7a..26240d458 100644 --- a/lib/worldstate/widgets/timers/arbitration_card.dart +++ b/lib/worldstate/widgets/timers/arbitration_card.dart @@ -3,25 +3,36 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:navis/l10n/l10n.dart'; import 'package:navis/worldstate/cubits/worldstate_cubit.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; class ArbitrationCard extends StatelessWidget { const ArbitrationCard({super.key}); @override Widget build(BuildContext context) { - return AppCard( - child: BlocBuilder( - builder: (context, state) { - final arbitration = switch (state) { - WorldstateSuccess() => state.worldstate.arbitration, - _ => null - }; + const gracePeriod = Duration(seconds: 1); + const oneYear = Duration(days: 365); + const archwingIcon = Padding( + padding: EdgeInsets.only(left: 6), + child: Icon( + WarframeSymbols.archwing, + color: Colors.blue, + size: 25, + ), + ); + return AppCard( + child: BlocSelector( + selector: (s) => switch (s) { + WorldstateSuccess() => s.worldstate.arbitration, + _ => null + }, + builder: (context, arbitration) { final now = DateTime.timestamp(); - var expiry = arbitration?.expiry ?? DateTime.now().add(expiryWait); - if (expiry.difference(now) > const Duration(days: 365)) { - expiry = DateTime.now().add(expiryWait); + var expiry = arbitration?.expiry ?? now.add(gracePeriod); + if (expiry.difference(now) > oneYear) { + expiry = now.add(gracePeriod); } return ListTile( @@ -29,15 +40,7 @@ class ArbitrationCard extends StatelessWidget { title: Row( mainAxisSize: MainAxisSize.min, children: [ - if (arbitration?.archwing ?? false) - const Padding( - padding: EdgeInsets.only(left: 6), - child: Icon( - WarframeSymbols.archwing, - color: Colors.blue, - size: 25, - ), - ), + if (arbitration?.archwing ?? false) archwingIcon, Text(arbitration?.node ?? ''), ], ), diff --git a/lib/worldstate/widgets/timers/event_card.dart b/lib/worldstate/widgets/timers/event_card.dart index f46d46bf4..832c4de28 100644 --- a/lib/worldstate/widgets/timers/event_card.dart +++ b/lib/worldstate/widgets/timers/event_card.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:navis/l10n/l10n.dart'; @@ -10,31 +9,15 @@ import 'package:warframestat_client/warframestat_client.dart'; class EventCard extends StatelessWidget { const EventCard({super.key}); - bool _buildWhen(SolsystemState previous, SolsystemState next) { - final previousWorldEvents = switch (previous) { - WorldstateSuccess() => previous.worldstate.events, - _ => [], - }; - - final nextWorldEvents = switch (next) { - WorldstateSuccess() => next.worldstate.events, - _ => [], - }; - - return !previousWorldEvents.equals(nextWorldEvents); - } - @override Widget build(BuildContext context) { return AppCard( - child: BlocBuilder( - buildWhen: _buildWhen, - builder: (context, state) { - final events = switch (state) { - WorldstateSuccess() => state.worldstate.events, - _ => [], - }; - + child: BlocSelector>( + selector: (s) => switch (s) { + WorldstateSuccess() => s.worldstate.events, + _ => [], + }, + builder: (context, events) { return Column( children: [ for (final event in events) diff --git a/lib/worldstate/widgets/timers/rewards.dart b/lib/worldstate/widgets/timers/rewards.dart index 75f08c51c..26ce38016 100644 --- a/lib/worldstate/widgets/timers/rewards.dart +++ b/lib/worldstate/widgets/timers/rewards.dart @@ -6,10 +6,10 @@ class DailyReward extends StatelessWidget { const DailyReward({super.key}); DateTime get endOfDay { - final now = DateTime.now().toUtc(); + final now = DateTime.timestamp(); return DateTime.utc(now.year, now.month, now.day, 23, 59, 59, 999) - .add(expiryWait); + .add(const Duration(seconds: 1)); } @override diff --git a/lib/worldstate/widgets/timers/steel_path.dart b/lib/worldstate/widgets/timers/steel_path.dart index 21f644450..5544910eb 100644 --- a/lib/worldstate/widgets/timers/steel_path.dart +++ b/lib/worldstate/widgets/timers/steel_path.dart @@ -3,30 +3,21 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:navis/l10n/l10n.dart'; import 'package:navis/worldstate/cubits/worldstate_cubit.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; class SteelPathCard extends StatelessWidget { const SteelPathCard({super.key}); - bool _buildWhen(SolsystemState p, SolsystemState n) { - if (p is WorldstateSuccess && n is WorldstateSuccess) { - return p.worldstate.steelPath.expiry != n.worldstate.steelPath.expiry; - } - - return false; - } - @override Widget build(BuildContext context) { return AppCard( - child: BlocBuilder( - buildWhen: _buildWhen, - builder: (context, state) { - final steelPath = switch (state) { - WorldstateSuccess() => state.worldstate.steelPath, - _ => null, - }; - - final expiry = steelPath?.expiry ?? DateTime.now(); + child: BlocSelector( + selector: (s) => switch (s) { + WorldstateSuccess() => s.worldstate.steelPath, + _ => null, + }, + builder: (context, steelPath) { + final expiry = steelPath?.expiry ?? DateTime.timestamp(); return ListTile( leading: const AppIcon(WarframeSymbols.sp_logo, size: 30), diff --git a/lib/worldstate/widgets/timers/trader_card.dart b/lib/worldstate/widgets/timers/trader_card.dart index f9a9f69f1..7f8b8e113 100644 --- a/lib/worldstate/widgets/timers/trader_card.dart +++ b/lib/worldstate/widgets/timers/trader_card.dart @@ -5,47 +5,38 @@ import 'package:navis/l10n/l10n.dart'; import 'package:navis/router/routes.dart'; import 'package:navis/worldstate/worldstate.dart'; import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; class TraderCard extends StatelessWidget { const TraderCard({super.key}); - bool _buildWhen(SolsystemState previous, SolsystemState current) { - if (previous is! WorldstateSuccess || current is! WorldstateSuccess) { - return false; - } - - final previousTrader = previous.worldstate.voidTraders.first; - final currentTrader = current.worldstate.voidTraders.first; - - return previousTrader.id != currentTrader.id; - } - @override Widget build(BuildContext context) { + const baroPurple = Color(0xFF82598b); final l10n = context.l10n; - return BlocBuilder( - buildWhen: _buildWhen, - builder: (context, state) { - final now = DateTime.now(); - final trader = switch (state) { - WorldstateSuccess() => state.worldstate.voidTraders.first, + return Card( + clipBehavior: Clip.antiAlias, + color: baroPurple, + child: BlocSelector?>( + selector: (s) => switch (s) { + WorldstateSuccess() => s.worldstate.voidTraders, _ => null, - }; + }, + builder: (context, traders) { + final now = DateTime.timestamp(); + final trader = traders?.first; + final isActive = trader?.active ?? false; - final isActive = trader?.active ?? false; - final date = MaterialLocalizations.of(context).formatFullDate( - isActive ? trader?.expiry ?? now : trader?.activation ?? now, - ); + final expiry = isActive ? trader?.expiry : trader?.activation; + final date = + MaterialLocalizations.of(context).formatFullDate(expiry ?? now); - final status = isActive ? l10n.baroLeavesOn : l10n.baroArrivesOn; - final title = '${l10n.baroTitle} ' - '${isActive ? '| ${trader?.location ?? ''}' : ''}'; + final status = isActive ? l10n.baroLeavesOn : l10n.baroArrivesOn; + final title = '${l10n.baroTitle} ' + '${isActive ? '| ${trader?.location ?? ''}' : ''}'; - return Card( - clipBehavior: Clip.antiAlias, - color: const Color(0xFF82598b), - child: InkWell( + return InkWell( onTap: isActive ? () => TraderPageRoute(trader?.inventory).push(context) : null, @@ -60,16 +51,15 @@ class TraderCard extends StatelessWidget { textColor: Colors.white, trailing: CountdownTimer( tooltip: l10n.countdownTooltip(date), - color: const Color(0xFF82598b), - expiry: - (isActive ? trader?.expiry : trader?.activation) ?? now, + color: baroPurple, + expiry: expiry ?? now, ), ), ), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/packages/navis_ui/lib/src/constants/constants.dart b/packages/navis_ui/lib/src/constants/constants.dart index c328aa3e6..0e292bb56 100644 --- a/packages/navis_ui/lib/src/constants/constants.dart +++ b/packages/navis_ui/lib/src/constants/constants.dart @@ -1,2 +1 @@ -export 'default_durations.dart'; export 'links.dart'; diff --git a/packages/navis_ui/lib/src/constants/default_durations.dart b/packages/navis_ui/lib/src/constants/default_durations.dart deleted file mode 100644 index 59776344e..000000000 --- a/packages/navis_ui/lib/src/constants/default_durations.dart +++ /dev/null @@ -1 +0,0 @@ -const expiryWait = Duration(seconds: 1); diff --git a/packages/notification_repository/pubspec.lock b/packages/notification_repository/pubspec.lock index aea0b98f9..3c0b5be73 100644 --- a/packages/notification_repository/pubspec.lock +++ b/packages/notification_repository/pubspec.lock @@ -350,5 +350,5 @@ packages: source: hosted version: "0.1.1" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/packages/warframestat_repository/lib/src/utils/extensions.dart b/packages/warframestat_repository/lib/src/utils/extensions.dart index e1b671472..df09aa617 100644 --- a/packages/warframestat_repository/lib/src/utils/extensions.dart +++ b/packages/warframestat_repository/lib/src/utils/extensions.dart @@ -27,3 +27,20 @@ extension InsertableXpItem on XpItem { return ArsenalItemXpCompanion.insert(uniqueName: uniqueName, xp: xp); } } + +extension WorldstateNavisX on Worldstate { + // bool get activeAcolytes => worldstate.enemyActive; + + bool get activeAlerts => alerts.isNotEmpty; + + bool get activeSales => flashSales.isNotEmpty; + + bool get arbitrationActive => + arbitration != null && !arbitration!.node.contains('000'); + + bool get eventsActive => events.isNotEmpty; + + bool get outpostDetected => sentientOutposts != null; + + bool get deepArchimedeaActive => deepArchimedea != null; +}