diff --git a/analysis_options.yaml b/analysis_options.yaml index 33e9f93a1..427da8050 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,5 +9,4 @@ analyzer: linter: rules: public_member_api_docs: false - library_private_types_in_public_api: false always_put_required_named_parameters_first: false 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..416f2904a 100644 --- a/lib/app/view/app_root.dart +++ b/lib/app/view/app_root.dart @@ -16,10 +16,10 @@ class NavisApp extends StatefulWidget { final AppRouter router; @override - _NavisAppState createState() => _NavisAppState(); + NavisAppState createState() => NavisAppState(); } -class _NavisAppState extends State with WidgetsBindingObserver { +class NavisAppState extends State with WidgetsBindingObserver { late Timer _timer; @override @@ -76,38 +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(); - } - @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 2b785c107..72fdbc0f4 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/profile/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,35 @@ 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 + void didChangeDependencies() { + super.didChangeDependencies(); + + _worldstateCubit.fetchWorldstate(); + + final state = _userSettingsCubit.state; + final username = switch (state) { + UserSettingsSuccess() => state.username, + _ => null, + }; + + if (username != null) { + _arsenalCubit.syncXpInfo(username); + } } @override @@ -35,6 +54,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..cee9876bc 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'; @@ -11,11 +12,13 @@ class RepositoryBootstrap extends StatefulWidget { super.key, required this.settings, required this.cache, + required this.routeObserver, required this.child, }); final UserSettings settings; final Box cache; + final RouteObserver> routeObserver; final Widget child; @override @@ -31,8 +34,10 @@ class _RepositoryBootstrapState extends State { super.initState(); _notifications = NotificationRepository(); - _warframestatRepository = - WarframestatRepository(client: SentryHttpClient()); + _warframestatRepository = WarframestatRepository( + client: SentryHttpClient(), + database: ArsenalDatabase(driftDatabase(name: 'arsenal')), + )..updateArsenalItems(); } @override @@ -42,6 +47,7 @@ class _RepositoryBootstrapState extends State { RepositoryProvider.value(value: widget.settings), RepositoryProvider.value(value: _warframestatRepository), RepositoryProvider.value(value: _notifications), + RepositoryProvider.value(value: widget.routeObserver), ], child: widget.child, ); diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 66e9a23e6..0ffc55181 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -9,13 +9,16 @@ import 'package:navis/app/app_observer.dart'; import 'package:navis/app/widgets/bloc_bootstrap.dart'; import 'package:navis/app/widgets/repo_bootstrap.dart'; import 'package:navis/firebase_options.dart'; +import 'package:navis/router/app_router.dart'; import 'package:navis/settings/settings.dart'; import 'package:navis/utils/utils.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sentry_hive/sentry_hive.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; -Future bootstrap(FutureOr Function() builder) async { +typedef BootstrapBuilder = FutureOr Function(AppRouter); + +Future bootstrap(BootstrapBuilder builder) async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Hive.initFlutter(); @@ -31,11 +34,18 @@ Future bootstrap(FutureOr Function() builder) async { storageDirectory: temp, ); + final observer = RouteObserver>(); + final router = AppRouter( + navigatorKey: GlobalKey(), + observer: observer, + ); + runApp( RepositoryBootstrap( settings: settings, cache: cache, - child: BlocBootstrap(child: await builder()), + routeObserver: observer, + child: BlocBootstrap(child: await builder(router)), ), ); } diff --git a/lib/codex/bloc/search_bloc.dart b/lib/codex/bloc/search_bloc.dart index e884219dc..3337f382f 100644 --- a/lib/codex/bloc/search_bloc.dart +++ b/lib/codex/bloc/search_bloc.dart @@ -38,7 +38,7 @@ class SearchBloc extends Bloc { try { final results = await ConnectionManager.call( - () async => repository.searchItems(text), + () => repository.searchItems(text), ); _originalResults = results; @@ -48,13 +48,12 @@ class SearchBloc extends Bloc { } on FormatException { emit(const CodexSearchError('Failed to parse server response')); } on Exception catch (error, stackTrace) { + emit(const CodexSearchError('Unknown Error occurred')); await Sentry.captureException( error, stackTrace: stackTrace, hint: Hint.withMap({'query': event.text}), ); - - emit(const CodexSearchError('Unknown Error occurred')); } } } @@ -77,11 +76,7 @@ class SearchBloc extends Bloc { } EventTransformer _waitForUser() { - return (event, mapper) { - return event - .debounceTime(kThemeAnimationDuration) - .distinct() - .flatMap(mapper); - }; + return (event, mapper) => + event.debounceTime(kThemeAnimationDuration).distinct().flatMap(mapper); } } diff --git a/lib/codex/cubit/item_cubit.dart b/lib/codex/cubit/item_cubit.dart index fbbd78751..7912c3f4e 100644 --- a/lib/codex/cubit/item_cubit.dart +++ b/lib/codex/cubit/item_cubit.dart @@ -17,17 +17,14 @@ class ItemCubit extends HydratedCubit { final WarframestatRepository repo; Future fetchItem() async { - final item = await _handleItemFetch(() async => repo.fetchItem(name)); - if (item == null) { - emit(const ItemNotFound()); - return; - } + final item = await _handleItemFetch(() => repo.fetchItem(name)); + if (item == null) return emit(const ItemNotFound()); emit(ItemFetchSuccess(item)); } Future fetchByName() async { - final items = await _handleItemFetch(() async => repo.searchItems(name)); + final items = await _handleItemFetch(() => repo.searchItems(name)); final item = items .where((item) => item.imageName != null) @@ -39,15 +36,11 @@ class ItemCubit extends HydratedCubit { } Future fetchIncarnonGenesis() async { - final items = await _handleItemFetch( - () async => repo.searchItems('Incarnon'), - ); + final items = await _handleItemFetch(() => repo.searchItems('Incarnon')); final item = items.where((item) => item.imageName != null).firstWhereOrNull( - (item) { - return name.replaceAll(' ', '') == item.name.replaceAll(' ', ''); - }, - ); + (item) => name.replaceAll(' ', '') == item.name.replaceAll(' ', ''), + ); if (item == null) return emit(const ItemNotFound()); diff --git a/lib/codex/views/codex_search_view.dart b/lib/codex/views/codex_search_view.dart index 2e0a8ad72..fc62467cc 100644 --- a/lib/codex/views/codex_search_view.dart +++ b/lib/codex/views/codex_search_view.dart @@ -7,7 +7,9 @@ import 'package:navis_ui/navis_ui.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; class CodexSearchPage extends StatelessWidget { - const CodexSearchPage({super.key}); + const CodexSearchPage({super.key, required this.query}); + + final String query; @override Widget build(BuildContext context) { @@ -17,7 +19,7 @@ class CodexSearchPage extends StatelessWidget { child: Scaffold( body: SafeArea( child: BlocProvider( - create: (_) => SearchBloc(repo), + create: (_) => SearchBloc(repo)..add(SearchCodex(query)), child: NestedScrollView( floatHeaderSlivers: true, headerSliverBuilder: (context, innerBoxIsScrolled) { @@ -27,15 +29,15 @@ 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(), + title: CodexSearchBar(hintText: query), ), ), ]; diff --git a/lib/codex/views/component_drops.dart b/lib/codex/views/component_drops.dart index 11164a79e..217cf73d0 100644 --- a/lib/codex/views/component_drops.dart +++ b/lib/codex/views/component_drops.dart @@ -1,9 +1,9 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:navis/codex/codex.dart'; import 'package:navis/l10n/l10n.dart'; -import 'package:warframestat_client/warframestat_client.dart'; +import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart' hide ItemNotFound; import 'package:warframestat_repository/warframestat_repository.dart'; class ComponentDrops extends StatelessWidget { @@ -12,39 +12,32 @@ class ComponentDrops extends StatelessWidget { final List drops; void _loadRelic(BuildContext context, String itemName) { - final teirReg = RegExp(r'\(([^)]*)\)'); - - final tier = teirReg.firstMatch(itemName)?.group(1); - final relic = - '${itemName.replaceAll(teirReg, '').trim()} ${tier ?? 'Intact'}'; - Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { return BlocProvider( - create: (context) => SearchBloc( + create: (context) => ItemCubit( + itemName, RepositoryProvider.of(context), - ), + )..fetchItem(), child: Scaffold( body: Builder( builder: (context) { - final l10n = context.l10n; - - context.watch().add(SearchCodex(relic)); - final state = context.watch().state; - - if (state is! CodexSuccessfulSearch) { - return const Center(child: CircularProgressIndicator()); - } + return BlocBuilder( + builder: (context, state) { + final l10n = context.l10n; - final item = state.results - .firstWhereOrNull((e) => e.name.contains(relic)); + if (state is! ItemFetchSuccess) { + return const Center(child: WarframeSpinner()); + } - if (item == null) { - return Center(child: Text(l10n.codexNoResults)); - } + if (state is ItemNotFound) { + return Center(child: Text(l10n.codexNoResults)); + } - return EntryView(item: item); + return EntryView(item: state.item); + }, + ); }, ), ), @@ -70,19 +63,15 @@ class ComponentDrops extends StatelessWidget { cacheExtent: cacheExtent, itemCount: drops.length, itemBuilder: (context, index) { - final dropName = drops[index].location; - final percentage = - ((drops[index].chance ?? 0) * 100).toStringAsFixed(2); + final drop = drops[index]; + final percentage = ((drop.chance ?? 0) * 100).toStringAsFixed(2); return ListTile( - title: Text(dropName), + title: Text(drop.location), subtitle: Text('$percentage% drop chance'), - onTap: !dropName.contains('Relic') - ? null - : () => _loadRelic( - context, - dropName.replaceFirst('Relic', '').trim(), - ), + onTap: drop.uniqueName != null + ? () => _loadRelic(context, drop.uniqueName!) + : null, dense: drops.length > densityThreshold, ); }, diff --git a/lib/codex/views/entry_view.dart b/lib/codex/views/entry_view.dart index 7536deb68..cf05205ac 100644 --- a/lib/codex/views/entry_view.dart +++ b/lib/codex/views/entry_view.dart @@ -52,15 +52,22 @@ class EntryViewOpenContainer extends StatelessWidget { class EntryView extends StatelessWidget { const EntryView({super.key, required this.item}); - final MinimalItem item; + final Item item; @override Widget build(BuildContext context) { final repo = RepositoryProvider.of(context); - return BlocProvider( - create: (context) => ItemCubit(item.uniqueName, repo)..fetchItem(), - child: Scaffold(body: SafeArea(child: _Overview(item: item))), + return Scaffold( + body: SafeArea( + child: item is MinimalItem + ? BlocProvider( + create: (context) => + ItemCubit(item.uniqueName, repo)..fetchItem(), + child: _Overview(item: item), + ) + : _Overview(item: item), + ), ); } } @@ -68,7 +75,15 @@ class EntryView extends StatelessWidget { class _Overview extends StatelessWidget { const _Overview({required this.item}); - final MinimalItem item; + final Item item; + + bool? _isVaulted() { + return switch (item) { + Relic() => (item as Relic).vaulted, + BuildableItem() => (item as BuildableItem).vaulted, + _ => false, + }; + } @override Widget build(BuildContext context) { @@ -96,7 +111,7 @@ class _Overview extends StatelessWidget { imageUrl: item.imageUrl, expandedHeight: height, disableInfo: isMod, - isVaulted: item.vaulted, + isVaulted: _isVaulted(), ), ), ]; diff --git a/lib/codex/widgets/codex_entry/entry_info.dart b/lib/codex/widgets/codex_entry/entry_info.dart index 6ba4c89c9..0fa94d792 100644 --- a/lib/codex/widgets/codex_entry/entry_info.dart +++ b/lib/codex/widgets/codex_entry/entry_info.dart @@ -37,6 +37,7 @@ class BasicItemInfo extends SliverPersistentHeaderDelegate { child: SizedBox( height: expandedHeight, child: Stack( + fit: StackFit.expand, children: [ AppBar( elevation: 0, @@ -88,9 +89,7 @@ class BasicItemInfo extends SliverPersistentHeaderDelegate { double get minExtent => kToolbarHeight; @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return false; - } + bool shouldRebuild(BasicItemInfo oldDelegate) => false; } class _EntryInfoContent extends StatelessWidget { diff --git a/lib/codex/widgets/codex_entry/relic_reward.dart b/lib/codex/widgets/codex_entry/relic_reward.dart index 4255da972..17dedf9b9 100644 --- a/lib/codex/widgets/codex_entry/relic_reward.dart +++ b/lib/codex/widgets/codex_entry/relic_reward.dart @@ -17,13 +17,19 @@ class RelicRewardWidget extends StatelessWidget { final color = r.rarity.toColor(); return ListTile( - title: Text(r.item.name), + textColor: color, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(r.item.name), + Text('${r.chance}%'), + ], + ), subtitle: LinearProgressIndicator( - value: r.chance / 100, color: color, borderRadius: BorderRadius.circular(4), + value: r.chance / 100, ), - textColor: color, ); }, ).toList(), diff --git a/lib/codex/widgets/codex_search/search_bar.dart b/lib/codex/widgets/codex_search/search_bar.dart index 0f8c43152..66627f3d3 100644 --- a/lib/codex/widgets/codex_search/search_bar.dart +++ b/lib/codex/widgets/codex_search/search_bar.dart @@ -5,17 +5,28 @@ import 'package:intl/intl.dart'; import 'package:navis/codex/codex.dart'; import 'package:navis/codex/utils/debouncer.dart'; import 'package:navis/l10n/l10n.dart'; +import 'package:navis/router/routes.dart'; import 'package:warframestat_client/warframestat_client.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; class CodexSearchBar extends StatefulWidget { - const CodexSearchBar({super.key}); + const CodexSearchBar({ + super.key, + this.focusNode, + this.controller, + this.hintText, + }); + + final FocusNode? focusNode; + final SearchController? controller; + final String? hintText; @override State createState() => _CodexSearchBarState(); } class _CodexSearchBarState extends State { + late final FocusNode _focusNode; late final SearchController _controller; String? _currentQuery; @@ -59,7 +70,11 @@ class _CodexSearchBarState extends State { void _onSubmitted(String query) { BlocProvider.of(context).add(SearchCodex(query)); - if (_controller.isOpen) _controller.closeView(_controller.text); + _controller.closeView(_controller.text); + + if (!Navigator.of(context).canPop()) { + CodexPageRoute(query).push(context); + } } List> _itemBuilder() { @@ -74,7 +89,8 @@ class _CodexSearchBarState extends State { @override void initState() { super.initState(); - _controller = SearchController(); + _focusNode = widget.focusNode ?? FocusNode(); + _controller = widget.controller ?? SearchController(); _debounceSearch = debounce(_search); } @@ -86,26 +102,51 @@ class _CodexSearchBarState extends State { padding: const EdgeInsets.all(8), child: BlocBuilder( builder: (context, state) { - return SearchAnchor.bar( + return SearchAnchor( searchController: _controller, textInputAction: TextInputAction.search, textCapitalization: TextCapitalization.words, - barLeading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), - ), - barHintText: l10n.codexHint, - onSubmitted: _onSubmitted, suggestionsBuilder: _suggestionsBuilder, - barTrailing: [ - if (state is CodexSuccessfulSearch) - PopupMenuButton( - icon: const Icon(Icons.filter_list), - itemBuilder: (_) => _itemBuilder(), - onSelected: (s) => BlocProvider.of(context) - .add(FilterResults(s)), - ), - ], + viewLeading: IconButton( + icon: const Icon(Icons.arrow_back_outlined), + onPressed: () { + _controller.closeView(null); + + if (!Navigator.canPop(context)) { + _controller.clear(); + _focusNode.unfocus(); + } + }, + ), + viewOnChanged: _search, + viewOnSubmitted: _onSubmitted, + builder: (context, controller) { + return SearchBar( + focusNode: _focusNode, + controller: controller, + onTap: _controller.openView, + onTapOutside: (_) => _focusNode.unfocus(), + hintText: widget.hintText ?? l10n.codexHint, + leading: Navigator.of(context).canPop() + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ) + : const Icon(Icons.search_rounded), + trailing: Navigator.of(context).canPop() + ? [ + if (state is CodexSuccessfulSearch) + PopupMenuButton( + icon: const Icon(Icons.filter_list), + itemBuilder: (_) => _itemBuilder(), + onSelected: (s) => + BlocProvider.of(context) + .add(FilterResults(s)), + ), + ] + : null, + ); + }, ); }, ), @@ -114,7 +155,9 @@ class _CodexSearchBarState extends State { @override void dispose() { + // Let the parent widget dispose of resources if they are provided + if (widget.focusNode == null) _focusNode.dispose(); + if (widget.controller == null) _controller.dispose(); super.dispose(); - _controller.dispose(); } } diff --git a/lib/explore/views/main_view.dart b/lib/explore/views/main_view.dart index cd60b471c..8187c9e72 100644 --- a/lib/explore/views/main_view.dart +++ b/lib/explore/views/main_view.dart @@ -37,14 +37,6 @@ class ExploreView extends StatelessWidget { onTap: () => const FishPageRoute().push(context), ), ), - AppCard( - child: ListTile( - leading: const Icon(Icons.search, size: iconSize), - title: Text(l10n.codexTitle), - subtitle: Text(l10n.codexDescription), - onTap: () => const CodexPageRoute().push(context), - ), - ), ], ); } diff --git a/lib/home/views/home.dart b/lib/home/views/home.dart index 46ecbd220..b979d11fc 100644 --- a/lib/home/views/home.dart +++ b/lib/home/views/home.dart @@ -1,23 +1,99 @@ import 'package:flutter/material.dart'; -import 'package:navis/home/widgets/widgets.dart'; -import 'package:navis_ui/navis_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:navis/codex/codex.dart'; +import 'package:navis/home/home.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { - return const HomeView(); + final repo = RepositoryProvider.of(context); + + return BlocProvider( + create: (_) => SearchBloc(repo), + child: const HomeView(), + ); } } -class HomeView extends StatelessWidget { +class HomeView extends StatefulWidget { const HomeView({super.key}); + @override + State createState() => _HomeViewState(); +} + +class _HomeViewState extends State with RouteAware { + late final FocusNode _focusNode; + late final SearchController _controller; + + RouteObserver>? _observer; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _controller = SearchController(); + + GoRouter.of(context).routerDelegate.addListener(_handleRouteChange); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _observer ??= + RepositoryProvider.of>>(context) + ..subscribe(this, ModalRoute.of(context)!); + } + + void _handleRouteChange() { + if (!mounted) return; + + final currentRoute = ModalRoute.of(context); + if (currentRoute?.isActive ?? false) { + _controller.clear(); + _focusNode.unfocus(); + } + } + @override Widget build(BuildContext context) { - return ListView( - children: const [NewsSection(), Gaps.gap16, ActivitiesSection()], + final children = [ + const NewsSection(), + const ActivitiesSection(), + const MasteryInProgressSection(), + ]; + + return CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + snap: true, + backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + titleSpacing: 0, + clipBehavior: Clip.none, + title: CodexSearchBar( + focusNode: _focusNode, + controller: _controller, + ), + ), + SliverList.list(children: children), + ], ); } + + @override + void dispose() { + _observer?.unsubscribe(this); + _observer = null; + + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } } 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/mastery_section.dart b/lib/home/widgets/mastery_section.dart new file mode 100644 index 000000000..f3e9934d1 --- /dev/null +++ b/lib/home/widgets/mastery_section.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/home/widgets/section.dart'; +import 'package:navis/profile/profile.dart'; +import 'package:navis/router/routes.dart'; +import 'package:navis/settings/cubit/user_settings_cubit.dart'; +import 'package:navis_ui/navis_ui.dart'; + +class MasteryInProgressSection extends StatelessWidget { + const MasteryInProgressSection({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) { + return switch (state) { + UserSettingsSuccess() => state.username, + _ => null + }; + }, + builder: (context, username) { + if (username == null) return const SizedBox.shrink(); + + return const MasteryInProgressContent(); + }, + ); + } +} + +class MasteryInProgressContent extends StatelessWidget { + const MasteryInProgressContent({super.key}); + + @override + Widget build(BuildContext context) { + return Section( + onTap: () => const MasteryPageRoute().push(context), + title: const Text('Mastery in progress'), + content: BlocBuilder( + builder: (context, state) { + const padding = EdgeInsets.symmetric(vertical: 16); + + 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.xpInfo.take(5)) ArsenalItemWidget(item: i), + ], + ); + }, + ), + ); + } +} 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/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..22897dd1f 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -1254,5 +1254,61 @@ "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": {} + }, + "itemRankSubtitle": "Rank {rank}", + "@itemRankSubtitle": { + "description": "Subtitle showing the item's rank", + "type": "text", + "placeholder": { + "rank": {} + } } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e3ca9a9eb..dcb0cd1d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:navis/app/app.dart'; import 'package:navis/bootstrap.dart'; -import 'package:navis/router/app_router.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_logging/sentry_logging.dart'; @@ -31,11 +30,9 @@ Future main() async { } await bootstrap( - () => DefaultAssetBundle( + (router) => DefaultAssetBundle( bundle: SentryAssetBundle(), - child: NavisApp( - router: AppRouter(navigatorKey: GlobalKey()), - ), + child: NavisApp(router: router), ), ); }, diff --git a/lib/profile/cubit/arsenal_cubit.dart b/lib/profile/cubit/arsenal_cubit.dart new file mode 100644 index 000000000..43098292b --- /dev/null +++ b/lib/profile/cubit/arsenal_cubit.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.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 HydratedCubit { + ArsenalCubit(this.repository) : super(ArsenalInitial()); + + final WarframestatRepository repository; + + late List _xpInfo; + + Future syncXpInfo(String username) async { + // Only update the state when it's not already loaded so the current stays + // in view until the new one is loaded + if (state is! ArsenalSuccess) emit(ArsenalUpdating()); + + try { + _xpInfo = await repository.syncXpInfo(username); + + _xpInfo + ..removeWhere((i) { + // Remove Excalibur prime because it is not obtainable so if doesn't + // exist in xp info it shouldn't display for the user + return i.item.name.contains('Excalibur Prime') && i.rank == 0; + }) + ..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); + }); + + emit(ArsenalSuccess(_xpInfo)); + } on Exception catch (e, stack) { + emit(ArsenalFailure()); + await Sentry.captureException(e, stackTrace: stack); + } + } + + @override + ArsenalState? fromJson(Map json) { + final progressList = json['inProgress'] as List; + final inProgess = List>.from(progressList).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.xpInfo + .map( + (p) => { + 'item': p.item.toJson(), + 'xp': p.xp, + 'missing': p.missing, + }, + ) + .toList(), + }; + } +} diff --git a/lib/profile/cubit/arsenal_state.dart b/lib/profile/cubit/arsenal_state.dart new file mode 100644 index 000000000..c63ad571a --- /dev/null +++ b/lib/profile/cubit/arsenal_state.dart @@ -0,0 +1,61 @@ +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 warframes => + xpInfo.where((i) => i.item.type == ItemType.warframes).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(); + + List get companions { + return xpInfo + .where( + (i) => switch (i.item.type) { + ItemType.sentinels || ItemType.pets => true, + _ => false + }, + ) + .toList(); + } + + List get kDrives => + xpInfo.where((i) => i.item.type == ItemType.kDriveComponent).toList(); + + List get archwing => + xpInfo.where((i) => i.item.type == ItemType.archwing).toList(); + + List get archGun => + xpInfo.where((i) => i.item.type == ItemType.archGun).toList(); + + List get archMelee => + xpInfo.where((i) => i.item.type == ItemType.archMelee).toList(); + + @override + List get props => [xpInfo]; +} + +final class ArsenalFailure extends ArsenalState {} diff --git a/lib/profile/profile.dart b/lib/profile/profile.dart new file mode 100644 index 000000000..52c58f274 --- /dev/null +++ b/lib/profile/profile.dart @@ -0,0 +1,3 @@ +export 'cubit/arsenal_cubit.dart'; +export 'views/mastery_page.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/profile/views/mastery_page.dart b/lib/profile/views/mastery_page.dart new file mode 100644 index 000000000..622d4804b --- /dev/null +++ b/lib/profile/views/mastery_page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/profile/profile.dart'; +import 'package:navis/profile/widgets/arsenal_items.dart'; +import 'package:navis_ui/navis_ui.dart'; + +class MasteryPage extends StatelessWidget { + const MasteryPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold(body: SafeArea(child: MasteryView())); + } +} + +class MasteryView extends StatelessWidget { + const MasteryView({super.key}); + + @override + Widget build(BuildContext context) { + const tabs = [ + Tab(text: 'Warframes'), + Tab(text: 'Primary'), + Tab(text: 'Secondary'), + Tab(text: 'Melee'), + Tab(text: 'Companions'), + Tab(text: 'K-Drive'), + Tab(text: 'Archwing'), + Tab(text: 'Arch-Gun'), + Tab(text: 'Arch-Melee'), + ]; + + return DefaultTabController( + length: tabs.length, + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: const SliverAppBar( + floating: true, + snap: true, + bottom: TabBar(isScrollable: true, tabs: tabs), + ), + ), + ]; + }, + body: BlocBuilder( + builder: (context, state) { + if (state is! ArsenalSuccess) { + return const Center(child: WarframeSpinner()); + } + + return TabBarView( + children: [ + ArsenalItems(items: state.warframes), + ArsenalItems(items: state.primaries), + ArsenalItems(items: state.secondary), + ArsenalItems(items: state.melee), + ArsenalItems(items: state.companions), + ArsenalItems(items: state.kDrives), + ArsenalItems(items: state.archwing), + ArsenalItems(items: state.archGun), + ArsenalItems(items: state.archMelee), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/profile/widgets/arsenal_item.dart b/lib/profile/widgets/arsenal_item.dart new file mode 100644 index 000000000..beb596cf8 --- /dev/null +++ b/lib/profile/widgets/arsenal_item.dart @@ -0,0 +1,42 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:navis/l10n/l10n.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.item}); + + final MasteryProgress item; + + @override + Widget build(BuildContext context) { + const leadingSize = 50.0; + + return AppCard( + color: item.rank == item.maxRank + ? Theme.of(context).colorScheme.secondaryContainer + : null, + child: ListTile( + leading: CachedNetworkImage( + imageUrl: item.item.imageUrl, + width: leadingSize, + errorWidget: (context, url, error) => const Icon( + WarframeSymbols.menu_LotusEmblem, + size: leadingSize, + ), + ), + title: Text(item.item.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item.rank != 0) Text(context.l10n.itemRankSubtitle(item.rank)), + if (item.rank != item.maxRank && item.rank != 0) + LinearProgressIndicator(value: item.rank / item.maxRank), + ], + ), + ), + ); + } +} diff --git a/lib/profile/widgets/arsenal_items.dart b/lib/profile/widgets/arsenal_items.dart new file mode 100644 index 000000000..525238722 --- /dev/null +++ b/lib/profile/widgets/arsenal_items.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:navis/profile/cubit/arsenal_cubit.dart'; +import 'package:navis/profile/widgets/arsenal_item.dart'; +import 'package:navis/settings/settings.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; + +class ArsenalItems extends StatelessWidget { + const ArsenalItems({super.key, required this.items}); + + final List items; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async { + final settings = BlocProvider.of(context).state; + final username = switch (settings) { + UserSettingsSuccess() => settings.username, + _ => null + }; + + if (username == null) return; + await BlocProvider.of(context).syncXpInfo(username); + }, + child: ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) => ArsenalItemWidget(item: items[index]), + ), + ); + } +} diff --git a/lib/profile/widgets/widgets.dart b/lib/profile/widgets/widgets.dart new file mode 100644 index 000000000..a47d7ae84 --- /dev/null +++ b/lib/profile/widgets/widgets.dart @@ -0,0 +1 @@ +export 'arsenal_item.dart'; diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index 72b1bd259..c816069c9 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:navis/router/routes.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; class AppRouter { AppRouter({ required GlobalKey navigatorKey, + required RouteObserver> observer, bool debugLogDiagnostics = false, }) { _goRouter = _routes( navigatorKey, + observer, debugLogDiagnostics, ); } @@ -20,12 +23,13 @@ class AppRouter { GoRouter _routes( GlobalKey navigatorKey, + RouteObserver> observer, bool debugLogDiagnostics, ) { return GoRouter( navigatorKey: navigatorKey, - initialLocation: const ActivitesPageRouteData().location, - observers: [SentryNavigatorObserver()], + initialLocation: const OverviewPageRouteData().location, + observers: [observer, SentryNavigatorObserver(), matomoObserver], debugLogDiagnostics: debugLogDiagnostics, routes: $appRoutes, ); diff --git a/lib/router/routes.dart b/lib/router/routes.dart index f329af6de..24dc589d7 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -1,10 +1,12 @@ import 'package:flutter/widgets.dart'; import 'package:go_router/go_router.dart'; +import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:navis/app/app.dart'; import 'package:navis/codex/views/codex_search_view.dart'; import 'package:navis/explore/views/fish_view.dart'; import 'package:navis/explore/views/main_view.dart'; import 'package:navis/home/home.dart'; +import 'package:navis/profile/views/mastery_page.dart'; import 'package:navis/settings/views/settings.dart'; import 'package:navis/synthtargets/synthtargets.dart'; import 'package:navis/worldstate/worldstate.dart'; @@ -80,7 +82,10 @@ class ActivitesPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const ActivitiesView(); + return const TraceableWidget( + actionName: 'ActivitesPage', + child: SafeArea(child: ActivitiesView()), + ); } } @@ -95,7 +100,7 @@ class OverviewPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const HomePage(); + return const TraceableWidget(actionName: 'HomePage', child: HomePage()); } } @@ -110,7 +115,10 @@ class ExplorePageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const ExplorePage(); + return const TraceableWidget( + actionName: 'ExplorePage', + child: SafeArea(child: ExplorePage()), + ); } } @@ -125,7 +133,10 @@ class SettingsPageRouteData extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const SettingsPage(); + return const TraceableWidget( + actionName: 'SettingsPage', + child: SafeArea(child: SettingsPage()), + ); } } @@ -138,7 +149,10 @@ class WorldEventPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return EventInformation(event: $extra); + return TraceableWidget( + actionName: 'EvenInformation(${$extra.description})', + child: EventInformation(event: $extra), + ); } } @@ -151,7 +165,10 @@ class SyndicatePageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return BountiesPage(syndicate: $extra); + return TraceableWidget( + actionName: 'BountiesPage(${$extra.syndicate})', + child: BountiesPage(syndicate: $extra), + ); } } @@ -164,7 +181,10 @@ class NightwavePageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return NightwavesPage(nightwave: $extra); + return TraceableWidget( + actionName: 'NightwavePage', + child: NightwavesPage(nightwave: $extra), + ); } } @@ -175,7 +195,10 @@ class SynthTargetsPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const SynthTargetsView(); + return const TraceableWidget( + actionName: 'SynthTargetsPage', + child: SynthTargetsView(), + ); } } @@ -188,7 +211,10 @@ class TraderPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return BaroInventory(inventory: $extra); + return TraceableWidget( + actionName: 'BaroInventoryPage', + child: BaroInventory(inventory: $extra), + ); } } @@ -199,18 +225,23 @@ class FishPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const FishPage(); + return const TraceableWidget(actionName: 'FishPage', child: FishPage()); } } @immutable @TypedGoRoute(name: 'codex', path: '/codex') class CodexPageRoute extends GoRouteData { - const CodexPageRoute(); + const CodexPageRoute(this.$extra); + + final String $extra; @override Widget build(BuildContext context, GoRouterState state) { - return const CodexSearchPage(); + return TraceableWidget( + actionName: 'CodexSearchPage', + child: CodexSearchPage(query: $extra), + ); } } @@ -221,6 +252,23 @@ class NewsPageRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const OrbiterNewsPage(); + return const TraceableWidget( + actionName: 'OrbiterPage', + child: OrbiterNewsPage(), + ); + } +} + +@immutable +@TypedGoRoute(name: 'mastery', path: '/mastery') +class MasteryPageRoute extends GoRouteData { + const MasteryPageRoute(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const TraceableWidget( + actionName: 'MasteryPage', + child: MasteryPage(), + ); } } diff --git a/lib/router/routes.g.dart b/lib/router/routes.g.dart index ba77b7888..e99aa3c13 100644 --- a/lib/router/routes.g.dart +++ b/lib/router/routes.g.dart @@ -1,7 +1,5 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: require_trailing_commas - part of 'routes.dart'; // ************************************************************************** @@ -18,6 +16,7 @@ List get $appRoutes => [ $fishPageRoute, $codexPageRoute, $newsPageRoute, + $masteryPageRoute, ]; RouteBase get $appShell => StatefulShellRouteData.$route( @@ -304,21 +303,24 @@ RouteBase get $codexPageRoute => GoRouteData.$route( ); extension $CodexPageRouteExtension on CodexPageRoute { - static CodexPageRoute _fromState(GoRouterState state) => - const CodexPageRoute(); + static CodexPageRoute _fromState(GoRouterState state) => CodexPageRoute( + state.extra as String, + ); String get location => GoRouteData.$location( '/codex', ); - void go(BuildContext context) => context.go(location); + void go(BuildContext context) => context.go(location, extra: $extra); - Future push(BuildContext context) => context.push(location); + Future push(BuildContext context) => + context.push(location, extra: $extra); void pushReplacement(BuildContext context) => - context.pushReplacement(location); + context.pushReplacement(location, extra: $extra); - void replace(BuildContext context) => context.replace(location); + void replace(BuildContext context) => + context.replace(location, extra: $extra); } RouteBase get $newsPageRoute => GoRouteData.$route( @@ -343,3 +345,27 @@ extension $NewsPageRouteExtension on NewsPageRoute { void replace(BuildContext context) => context.replace(location); } + +RouteBase get $masteryPageRoute => GoRouteData.$route( + path: '/mastery', + name: 'mastery', + factory: $MasteryPageRouteExtension._fromState, + ); + +extension $MasteryPageRouteExtension on MasteryPageRoute { + static MasteryPageRoute _fromState(GoRouterState state) => + const MasteryPageRoute(); + + String get location => GoRouteData.$location( + '/mastery', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} diff --git a/lib/settings/cubit/user_settings_cubit.dart b/lib/settings/cubit/user_settings_cubit.dart index 9a4e1f40b..0795ba41c 100644 --- a/lib/settings/cubit/user_settings_cubit.dart +++ b/lib/settings/cubit/user_settings_cubit.dart @@ -9,13 +9,31 @@ 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) { + if (_settings.language == language) return; + _settings.language = language; emit((state as UserSettingsSuccess).copyWith(language: _settings.language)); } @@ -39,20 +57,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..ae403f1ab 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 ?? this.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 ec3dc2211..e81799432 100644 --- a/lib/settings/views/settings.dart +++ b/lib/settings/views/settings.dart @@ -12,11 +12,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 const Scaffold(body: SafeArea(child: _SettingsView())); + return const _SettingsView(); } } @@ -50,6 +48,11 @@ class _SettingsView extends StatelessWidget { 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 @@ -74,6 +77,16 @@ 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, + ), + ], + ), 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..d9b64236e --- /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/l10n/l10n.dart'; +import 'package:navis/profile/profile.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).syncXpInfo(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/lib/utils/sentry_hydrated_storage.dart b/lib/utils/sentry_hydrated_storage.dart index 163dcd646..5f6f64e1a 100644 --- a/lib/utils/sentry_hydrated_storage.dart +++ b/lib/utils/sentry_hydrated_storage.dart @@ -29,13 +29,13 @@ class SentryHydratedStorage implements Storage { if (storageDirectory == webStorageDirectory) { box = await SentryHive.openBox( 'hydrated_box', - encryptionCipher: encryptionCipher, + encryptionCipher: encryptionCipher as HiveCipher?, ); } else { SentryHive.init(storageDirectory.path); box = await SentryHive.openBox( 'hydrated_box', - encryptionCipher: encryptionCipher, + encryptionCipher: encryptionCipher as HiveCipher?, ); await _migrate(storageDirectory, box); } 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/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/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/lib/src/notification_repository.dart b/packages/notification_repository/lib/src/notification_repository.dart index 8e77aa36b..4d43dd7c9 100644 --- a/packages/notification_repository/lib/src/notification_repository.dart +++ b/packages/notification_repository/lib/src/notification_repository.dart @@ -4,9 +4,6 @@ import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:notification_repository/src/topics.dart'; -// TODO(SlayerOrnstein): We might need to use other services that also provide -// ADM for amazon devices - /// {@template notification_repository} /// Main entry to start push notifications via firebase. /// 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..1f558c1ad --- /dev/null +++ b/packages/warframestat_repository/lib/src/arsenal_database.g.dart @@ -0,0 +1,1318 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: require_trailing_commas, document_ignores, unused_import + +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/models/regions.dart b/packages/warframestat_repository/lib/src/models/regions.dart new file mode 100644 index 000000000..c2836e874 --- /dev/null +++ b/packages/warframestat_repository/lib/src/models/regions.dart @@ -0,0 +1,89 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'regions.g.dart'; + +@JsonSerializable() +class CraigJunction extends Equatable { + const CraigJunction({ + required this.uniqueName, + required this.name, + required this.systemIndex, + required this.minEnemyLevel, + required this.maxEnemyLevel, + required this.mastery, + required this.targetSystemIndex, + }); + + factory CraigJunction.fromJson(Map json) { + return _$CraigJunctionFromJson(json); + } + + final String uniqueName; + final String name; + final int systemIndex; + final int minEnemyLevel; + final int maxEnemyLevel; + final int mastery; + final int targetSystemIndex; + + Map toJson() => _$CraigJunctionToJson(this); + + @override + List get props => [ + uniqueName, + name, + systemIndex, + minEnemyLevel, + maxEnemyLevel, + mastery, + targetSystemIndex, + ]; +} + +@JsonSerializable() +class CraigNode extends Equatable { + const CraigNode({ + required this.uniqueName, + required this.name, + required this.systemIndex, + required this.minEnemyLevel, + required this.maxEnemyLevel, + required this.mastery, + required this.nodeType, + required this.masteryReq, + required this.missionIndex, + required this.factionIndex, + }); + + factory CraigNode.fromJson(Map json) { + return _$CraigNodeFromJson(json); + } + + final String uniqueName; + final String name; + final int systemIndex; + final int minEnemyLevel; + final int maxEnemyLevel; + final int mastery; + final int nodeType; + final int masteryReq; + final int missionIndex; + final int factionIndex; + + Map toJson() => _$CraigNodeToJson(this); + + @override + List get props => [ + uniqueName, + name, + systemIndex, + minEnemyLevel, + maxEnemyLevel, + mastery, + nodeType, + masteryReq, + missionIndex, + factionIndex, + ]; +} diff --git a/packages/warframestat_repository/lib/src/models/regions.g.dart b/packages/warframestat_repository/lib/src/models/regions.g.dart new file mode 100644 index 000000000..12693f67e --- /dev/null +++ b/packages/warframestat_repository/lib/src/models/regions.g.dart @@ -0,0 +1,57 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: require_trailing_commas, document_ignores, unused_import + +part of 'regions.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CraigJunction _$CraigJunctionFromJson(Map json) => + CraigJunction( + uniqueName: json['uniqueName'] as String, + name: json['name'] as String, + systemIndex: (json['systemIndex'] as num).toInt(), + minEnemyLevel: (json['minEnemyLevel'] as num).toInt(), + maxEnemyLevel: (json['maxEnemyLevel'] as num).toInt(), + mastery: (json['mastery'] as num).toInt(), + targetSystemIndex: (json['targetSystemIndex'] as num).toInt(), + ); + +Map _$CraigJunctionToJson(CraigJunction instance) => + { + 'uniqueName': instance.uniqueName, + 'name': instance.name, + 'systemIndex': instance.systemIndex, + 'minEnemyLevel': instance.minEnemyLevel, + 'maxEnemyLevel': instance.maxEnemyLevel, + 'mastery': instance.mastery, + 'targetSystemIndex': instance.targetSystemIndex, + }; + +CraigNode _$CraigNodeFromJson(Map json) => CraigNode( + uniqueName: json['uniqueName'] as String, + name: json['name'] as String, + systemIndex: (json['systemIndex'] as num).toInt(), + minEnemyLevel: (json['minEnemyLevel'] as num).toInt(), + maxEnemyLevel: (json['maxEnemyLevel'] as num).toInt(), + mastery: (json['mastery'] as num).toInt(), + nodeType: (json['nodeType'] as num).toInt(), + masteryReq: (json['masteryReq'] as num).toInt(), + missionIndex: (json['missionIndex'] as num).toInt(), + factionIndex: (json['factionIndex'] as num).toInt(), + ); + +Map _$CraigNodeToJson(CraigNode instance) => { + 'uniqueName': instance.uniqueName, + 'name': instance.name, + 'systemIndex': instance.systemIndex, + 'minEnemyLevel': instance.minEnemyLevel, + 'maxEnemyLevel': instance.maxEnemyLevel, + 'mastery': instance.mastery, + 'nodeType': instance.nodeType, + 'masteryReq': instance.masteryReq, + 'missionIndex': instance.missionIndex, + 'factionIndex': instance.factionIndex, + }; diff --git a/packages/warframestat_repository/lib/src/repository.dart b/packages/warframestat_repository/lib/src/repository.dart index 2e0b3cd67..1ef6c18cf 100644 --- a/packages/warframestat_repository/lib/src/repository.dart +++ b/packages/warframestat_repository/lib/src/repository.dart @@ -1,23 +1,36 @@ +import 'dart:convert'; +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/models/regions.dart'; +import 'package:warframestat_repository/src/utils/utils.dart'; /// const userAgent = 'navis'; +const _name = 'WarframestatRepository'; + +typedef CraigRegion = ({List nodes, List junctions}); /// {@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 @@ -92,4 +105,71 @@ 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 { + const stallTime = Duration(days: 7); + + final lastUpdate = await _database.lastUpdate(); + final lastUpdateElapsed = + lastUpdate?.difference(DateTime.timestamp()) ?? stallTime; + + final needsUpdate = lastUpdateElapsed >= stallTime || 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 || i.name.contains('Helminth')); + + await _database.updateItems(items); + await _database.updateTimeStamp(); + } + + Future> syncXpInfo(String username) async { + developer.log('syncing xp info', name: _name); + + // Share the same catch as profile, XP info doesn't change often and this + // keeps the profile hydrated + final xpInfo = (await fetchProfile(username)).loadout.xpInfo; + await _database.updateXp(xpInfo); + + return _database.fetchArsenal(); + } + + Future fetchRegions() async { + final client = await _cacheClient(const Duration(days: 30)); + final res = + await client.get(Uri.parse('https://cdn.truemaster.app/regions.json')); + + final json = jsonDecode(res.body) as Map; + final nodes = _jsonMapList(json['nodes'] as List); + final junctions = _jsonMapList(json['junctions'] as List); + + return ( + nodes: nodes.map(CraigNode.fromJson).toList(), + junctions: junctions.map(CraigJunction.fromJson).toList() + ); + } + + List> _jsonMapList(List list) { + return List>.from(list); + } } 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..df09aa617 --- /dev/null +++ b/packages/warframestat_repository/lib/src/utils/extensions.dart @@ -0,0 +1,46 @@ +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); + } +} + +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; +} 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..24b0cc22d 100644 --- a/packages/warframestat_repository/pubspec.yaml +++ b/packages/warframestat_repository/pubspec.yaml @@ -4,17 +4,22 @@ 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 + equatable: ^2.0.7 hive_ce: ^2.7.0+1 http: ^1.2.1 - warframestat_client: ^3.8.11 + json_annotation: ^4.9.0 + warframestat_client: ^3.15.2 dev_dependencies: build_runner: ^2.4.13 + drift_dev: ^2.22.1 hive_ce_generator: ^1.7.0 + json_serializable: ^6.9.0 mocktail: ^1.0.3 path: ^1.9.0 test: ^1.25.2 diff --git a/packages/warframestat_repository/test/warframestat_repository_test.dart b/packages/warframestat_repository/test/warframestat_repository_test.dart index 3dcbf993c..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 { diff --git a/pubspec.lock b/pubspec.lock index 3df0ecc9e..f9f88051c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -98,18 +98,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "9.0.0" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" url: "https://pub.dev" source: hosted - version: "9.1.7" + version: "10.0.0" boolean_selector: dependency: transitive description: @@ -390,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: @@ -518,10 +534,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + sha256: "153856bdaac302bbdc58a1d1403d50c40557254aa05eaeed40515d88a25a526b" url: "https://pub.dev" source: hosted - version: "8.1.6" + version: "9.0.0" flutter_cache_manager: dependency: transitive description: @@ -765,10 +781,10 @@ packages: dependency: "direct main" description: name: hydrated_bloc - sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c + sha256: "5401d5df7d800256a0d1ae16b5d8b518dd44c50d412fe96c6a96d8a4ea85a938" url: "https://pub.dev" source: hosted - version: "9.1.5" + version: "10.0.0" image: dependency: transitive description: @@ -1139,10 +1155,10 @@ packages: dependency: "direct main" description: name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.27.7" sentry: dependency: transitive description: @@ -1372,6 +1388,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: @@ -1568,10 +1600,10 @@ packages: dependency: "direct main" description: name: warframestat_client - sha256: "7e4daf24aeda89009e5573f5a12c95d2e1d31ca9c59255fecf13437efda7e425" + sha256: "3ee791278d7c53524644b45c2634659c1497823d6d9cebce86d9d13becc1b577" url: "https://pub.dev" source: hosted - version: "3.15.3" + version: "3.16.0" warframestat_repository: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5d6d798a1..3d5b9e1db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,10 +9,11 @@ environment: dependencies: animations: ^2.0.11 black_hole_flutter: ^1.1.0 - bloc: ^8.1.4 + bloc: ^9.0.0 cached_network_image: ^3.3.1 collection: ^1.19.0 connecteo: ^3.0.0 + drift_flutter: ^0.2.2 dynamic_color: ^1.7.0 equatable: ^2.0.5 feedback_sentry: ^3.1.0 @@ -21,7 +22,7 @@ dependencies: path: packages/fish_repository flutter: sdk: flutter - flutter_bloc: ^8.1.6 + flutter_bloc: ^9.0.0 flutter_localizations: sdk: flutter flutter_settings_ui: ^3.0.1 @@ -29,7 +30,7 @@ dependencies: go_router: ^14.6.2 hive: ^2.2.3 hive_flutter: ^1.1.0 - hydrated_bloc: ^9.1.5 + hydrated_bloc: ^10.0.0 intl: ^0.19.0 logging: ^1.3.0 matomo_tracker: ^5.1.0 @@ -40,21 +41,20 @@ dependencies: package_info_plus: ^8.1.2 path_provider: ^2.1.5 responsive_builder: ^0.7.0 - rxdart: ^0.28.0 + rxdart: ^0.27.7 sentry_flutter: ^8.12.0 sentry_hive: ^8.12.0 sentry_logging: ^8.12.0 simple_icons: ^10.1.3 - warframestat_client: ^3.15.3 + warframestat_client: ^3.16.0 warframestat_repository: path: packages/warframestat_repository dependency_overrides: meta: 1.15.0 - rxdart: 0.28.0 dev_dependencies: - bloc_test: ^9.1.6 + bloc_test: ^10.0.0 build_runner: ^2.4.14 build_verify: ^3.1.0 commitlint_cli: ^0.8.1