From 9a531a703082df713f807d8631155b1218a714f2 Mon Sep 17 00:00:00 2001 From: Ornstein <6075693+SlayerOrnstein@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:02:52 -0500 Subject: [PATCH] feat: daily deals, rewards and circuit items now open the codex (#548) ci: bump version direclty semantic-release-pub just broke out of now where so for the time being new pipline with more control --- .github/workflows/build_test.yml | 7 +- .github/workflows/release.yml | 41 ++++- .github/workflows/test.yml | 10 +- .releaserc.yaml | 7 - ios/Runner.xcodeproj/project.pbxproj | 22 ++- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- ios/Runner/AppDelegate.swift | 2 +- lib/app/widgets/bloc_bootstrap.dart | 3 - lib/codex/cubit/item_cubit.dart | 76 ++++++-- lib/codex/cubit/item_state.dart | 8 +- lib/codex/views/codex_search_view.dart | 11 +- lib/codex/views/entry_view.dart | 28 ++- .../widgets/codex_entry/frame_stats.dart | 7 + lib/utils/item_extensions.dart | 18 +- lib/worldstate/cubits/darvodeal_cubit.dart | 2 - .../cubits/deals/darvodeal_cubit.dart | 61 ------- .../cubits/deals/darvodeal_state.dart | 29 --- lib/worldstate/views/timers.dart | 2 +- .../widgets/timers/alerts_card.dart | 143 +++++++++++---- .../widgets/timers/darvo_deal_card.dart | 71 +++----- .../widgets/timers/duviri_circuit.dart | 171 ++++++++++++++++++ .../widgets/timers/duviri_cycle.dart | 118 ------------ lib/worldstate/widgets/widgets.dart | 2 +- lib/worldstate/worldstate.dart | 1 - .../lib/src/cache_client.dart | 2 +- .../lib/src/repository.dart | 26 +-- .../test/warframestat_repository_test.dart | 42 ----- pubspec.lock | 24 +-- pubspec.yaml | 2 +- 29 files changed, 496 insertions(+), 442 deletions(-) delete mode 100644 lib/worldstate/cubits/darvodeal_cubit.dart delete mode 100644 lib/worldstate/cubits/deals/darvodeal_cubit.dart delete mode 100644 lib/worldstate/cubits/deals/darvodeal_state.dart create mode 100644 lib/worldstate/widgets/timers/duviri_circuit.dart delete mode 100644 lib/worldstate/widgets/timers/duviri_cycle.dart diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 989960716..b61d9ee26 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -48,16 +48,11 @@ jobs: path: "build/app/outputs/flutter-apk/app-*-dev-release.apk" ios: - runs-on: macos-13 + runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup Xcode version - uses: maxim-lobanov/setup-xcode@v1.6.0 - with: - xcode-version: "^15.0.1" - - uses: ruby/setup-ruby@v1 with: ruby-version: 3.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8b8f8973..faeebb82e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,12 +11,47 @@ jobs: with: fetch-tags: true token: ${{ secrets.GH_TOKEN }} + - name: Semantic Release id: semantic uses: cycjimmy/semantic-release-action@v4.1.1 env: GH_TOKEN: ${{ secrets.GH_TOKEN }} with: - extra_plugins: | - @semantic-release/git - semantic-release-pub + semantic_version: 24.2.0 + + - name: Bump version + if: steps.semantic.outputs.new_release_published == 'true' + env: + NEW_RELEASE: ${{ steps.semantic.outputs.new_release_version }} + run: | + version=$(grep '^version:' pubspec.yaml | awk '{print $2}') + version_code=$(echo $version | sed 's/.*+//') + + new_version="${NEW_RELEASE}+$((version_code + 1))" + + sed -i "s/^version: .*/version: ${new_version}/" pubspec.yaml + + + - name: Commit and push changes + if: steps.semantic.outputs.new_release_published == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NOTES: ${{ steps.semantic.outputs.new_release_notes}} + NEW_RELEASE: ${{ steps.semantic.outputs.new_release_version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add pubspec.yaml + git commit -m "chore(release): $NEW_RELEASE [skip ci]" -m "$RELEASE_NOTES" --no-verify + git push + + - name: Create Release + if: steps.semantic.outputs.new_release_published == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_NOTES: ${{ steps.semantic.outputs.new_release_notes }} + NEW_RELEASE: ${{ steps.semantic.outputs.new_release_version }} + PRERELEASE: ${{ contains(steps.semantic.outputs.new_release_channel, 'beta') }} + run: gh release create v$NEW_RELEASE --notes "$RELEASE_NOTES" --target ${{ github.sha }} $([ "$PRERELEASE" = "true" ] && echo "--prerelease") + \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a4393f45..198c74290 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,12 +26,12 @@ jobs: run: flutter pub get - name: Unit Test app run: flutter test --coverage - build_test: - needs: [test] - secrets: inherit - uses: ./.github/workflows/build_test.yml + # build_test: + # needs: [test] + # secrets: inherit + # uses: ./.github/workflows/build_test.yml release: if: github.event_name != 'pull_request' - needs: [test, build_test] + needs: [test] secrets: inherit uses: ./.github/workflows/release.yml \ No newline at end of file diff --git a/.releaserc.yaml b/.releaserc.yaml index 641418247..67eff3b9e 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -5,13 +5,6 @@ plugins: release: patch - - "@semantic-release/release-notes-generator" - presets: conventionalcommits - - "@semantic-release/github" - - - "semantic-release-pub" - - publishPub: false - updateBuildNumber: true - - - "@semantic-release/git" - - assets: ["pubspec.yaml"] - message: "chore(release): ${nextRelease.version}" branches: - name: master - name: beta diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d5489f59c..ffd7593aa 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -203,6 +203,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 5AC57969B9426D71894E4F7A /* [CP] Embed Pods Frameworks */, + F565C2D5BD8150893B1356B5 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -220,7 +221,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -367,6 +368,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + F565C2D5BD8150893B1356B5 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09b..8e3ca5dfe 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { late WorldstateCubit _worldstateCubit; - late DarvodealCubit _darvodealCubit; late UserSettingsCubit _userSettingsCubit; @override @@ -27,7 +26,6 @@ class _BlocBootstrapState extends State { final usersettings = RepositoryProvider.of(context); _worldstateCubit = WorldstateCubit(worldstateRepo); - _darvodealCubit = DarvodealCubit(worldstateRepo); _userSettingsCubit = UserSettingsCubit(usersettings); } @@ -36,7 +34,6 @@ class _BlocBootstrapState extends State { return MultiBlocProvider( providers: [ BlocProvider.value(value: _worldstateCubit), - BlocProvider.value(value: _darvodealCubit), BlocProvider.value(value: _userSettingsCubit), ], child: widget.child, diff --git a/lib/codex/cubit/item_cubit.dart b/lib/codex/cubit/item_cubit.dart index f66160a99..ca64c378b 100644 --- a/lib/codex/cubit/item_cubit.dart +++ b/lib/codex/cubit/item_cubit.dart @@ -1,5 +1,8 @@ -import 'package:bloc/bloc.dart'; +import 'dart:async'; + +import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:navis/utils/utils.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:warframestat_client/warframestat_client.dart'; @@ -7,31 +10,76 @@ import 'package:warframestat_repository/warframestat_repository.dart'; part 'item_state.dart'; -class ItemCubit extends Cubit { - ItemCubit(this.repo) : super(ItemInitial()); +class ItemCubit extends HydratedCubit { + ItemCubit(this.name, this.repo) : super(ItemInitial()); + final String name; final WarframestatRepository repo; - Future fetchItem(String uniqueName) async { - try { - Item item; - if (await ConnectionManager.hasInternetConnection) { - item = await repo.fetchItem(uniqueName); - } + Future fetchItem() async { + final item = await _handleItemFetch(() async => repo.fetchItem(name)); - item = await ConnectionManager.call( - () async => repo.fetchItem(uniqueName), - ); + emit(ItemFetchSuccess(item)); + } + + Future fetchByName() async { + final items = await _handleItemFetch(() async => repo.searchItems(name)); + + final item = items + .where((item) => item.imageName != null) + .firstWhereOrNull((item) => name == item.name); + + if (item == null) return emit(const NoItemFound()); + + emit(ItemFetchSuccess(item)); + } + + Future fetchIncarnonGenesis() async { + final items = await _handleItemFetch( + () async => repo.searchItems('Incarnon'), + ); + + final item = items.where((item) => item.imageName != null).firstWhereOrNull( + (item) { + return name.replaceAll(' ', '') == item.name.replaceAll(' ', ''); + }, + ); + + if (item == null) return emit(const NoItemFound()); - emit(ItemFetchSucess(item)); + emit(ItemFetchSuccess(item)); + } + + Future _handleItemFetch(FutureOr Function() compute) async { + try { + return ConnectionManager.call(compute); } catch (e, s) { await Sentry.captureException( e, stackTrace: s, - hint: Hint.withMap({'uniqueName': uniqueName}), + hint: Hint.withMap({'query': name}), ); emit(const ItemFetchFailure('Failed to parse item')); + + rethrow; } } + + @override + String get id => name; + + @override + ItemState? fromJson(Map json) { + final item = MinimalItem.fromJson(json); + + return ItemFetchSuccess(item); + } + + @override + Map? toJson(ItemState state) { + if (state is! ItemFetchSuccess) return null; + + return state.item.toJson(); + } } diff --git a/lib/codex/cubit/item_state.dart b/lib/codex/cubit/item_state.dart index ab5e4c771..3d4741a34 100644 --- a/lib/codex/cubit/item_state.dart +++ b/lib/codex/cubit/item_state.dart @@ -9,8 +9,8 @@ sealed class ItemState extends Equatable { final class ItemInitial extends ItemState {} -final class ItemFetchSucess extends ItemState { - const ItemFetchSucess(this.item); +final class ItemFetchSuccess extends ItemState { + const ItemFetchSuccess(this.item); final Item item; @@ -26,3 +26,7 @@ final class ItemFetchFailure extends ItemState { @override List get props => [message]; } + +final class NoItemFound extends ItemState { + const NoItemFound(); +} diff --git a/lib/codex/views/codex_search_view.dart b/lib/codex/views/codex_search_view.dart index 20d224b01..0c9d0de57 100644 --- a/lib/codex/views/codex_search_view.dart +++ b/lib/codex/views/codex_search_view.dart @@ -1,4 +1,3 @@ -import 'package:animations/animations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; @@ -69,13 +68,9 @@ class CodexSearchView extends StatelessWidget { SliverList.builder( itemCount: state.results.length, itemBuilder: (BuildContext context, int index) { - return OpenContainer( - closedColor: Theme.of(context).colorScheme.surface, - openColor: Theme.of(context).colorScheme.surface, - openBuilder: (_, __) { - return EntryView(item: state.results[index]); - }, - closedBuilder: (_, onTap) { + return EntryViewOpenContainer( + item: state.results[index], + builder: (_, onTap) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: CodexResult( diff --git a/lib/codex/views/entry_view.dart b/lib/codex/views/entry_view.dart index d765ef3e2..965725e77 100644 --- a/lib/codex/views/entry_view.dart +++ b/lib/codex/views/entry_view.dart @@ -1,3 +1,4 @@ +import 'package:animations/animations.dart'; import 'package:black_hole_flutter/black_hole_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,19 +7,38 @@ import 'package:navis_ui/navis_ui.dart'; import 'package:warframestat_client/warframestat_client.dart'; import 'package:warframestat_repository/warframestat_repository.dart'; +class EntryViewOpenContainer extends StatelessWidget { + const EntryViewOpenContainer({ + super.key, + required this.item, + required this.builder, + }); + + final MinimalItem item; + final Widget Function(BuildContext, void Function()) builder; + + @override + Widget build(BuildContext context) { + return OpenContainer( + closedColor: Theme.of(context).colorScheme.surface, + openColor: Theme.of(context).colorScheme.surface, + openBuilder: (_, __) => EntryView(item: item), + closedBuilder: builder, + ); + } +} + class EntryView extends StatelessWidget { const EntryView({super.key, required this.item}); final MinimalItem item; - static const route = '/codexEntry'; - @override Widget build(BuildContext context) { final repo = RepositoryProvider.of(context); return BlocProvider( - create: (context) => ItemCubit(repo)..fetchItem(item.uniqueName), + create: (context) => ItemCubit(item.uniqueName, repo)..fetchItem(), child: Scaffold(body: SafeArea(child: _Overview(item: item))), ); } @@ -66,7 +86,7 @@ class _Overview extends StatelessWidget { return Center(child: Text(state.message)); } - if (state is! ItemFetchSucess) { + if (state is! ItemFetchSuccess) { return const Center( child: CircularProgressIndicator.adaptive(), ); diff --git a/lib/codex/widgets/codex_entry/frame_stats.dart b/lib/codex/widgets/codex_entry/frame_stats.dart index fb5586f71..2aa74a488 100644 --- a/lib/codex/widgets/codex_entry/frame_stats.dart +++ b/lib/codex/widgets/codex_entry/frame_stats.dart @@ -1,10 +1,12 @@ // ignore_for_file: prefer-moving-to-variable +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:navis/codex/utils/stats.dart'; import 'package:navis/codex/widgets/codex_entry/polarity.dart'; import 'package:navis/codex/widgets/codex_entry/preinstalled_polarities.dart'; import 'package:navis/codex/widgets/codex_entry/stats.dart'; import 'package:navis/l10n/l10n.dart'; +import 'package:navis/utils/utils.dart'; import 'package:navis_ui/navis_ui.dart'; import 'package:warframestat_client/warframestat_client.dart' hide Polarity; @@ -77,6 +79,11 @@ class FrameStats extends StatelessWidget { ), for (final ability in powerSuit.abilities) ListTile( + leading: CachedNetworkImage( + imageUrl: ability.imageUrl, + height: 100, + width: 50, + ), title: Text(ability.name), subtitle: Text(ability.description), dense: true, diff --git a/lib/utils/item_extensions.dart b/lib/utils/item_extensions.dart index c3265a683..ebdfff1c4 100644 --- a/lib/utils/item_extensions.dart +++ b/lib/utils/item_extensions.dart @@ -4,12 +4,18 @@ import 'package:warframestat_client/warframestat_client.dart'; const defaultImage = 'https://raw.githubusercontent.com/WFCD/genesis-assets/master/img/menu/LotusEmblem.png'; +String _imageUrl(String? imageName) { + if (imageName == null) { + return defaultImage.optimize(); + } + + return imageName.warframeItemsCdn().optimize(); +} + extension ItemX on Item { - String get imageUrl { - if (imageName == null) { - return defaultImage.optimize(); - } + String get imageUrl => _imageUrl(imageName); +} - return imageName!.warframeItemsCdn().optimize(); - } +extension AbilityX on Ability { + String get imageUrl => _imageUrl(imageName); } diff --git a/lib/worldstate/cubits/darvodeal_cubit.dart b/lib/worldstate/cubits/darvodeal_cubit.dart deleted file mode 100644 index 366df8502..000000000 --- a/lib/worldstate/cubits/darvodeal_cubit.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'deals/darvodeal_cubit.dart'; -export 'deals/darvodeal_state.dart'; diff --git a/lib/worldstate/cubits/deals/darvodeal_cubit.dart b/lib/worldstate/cubits/deals/darvodeal_cubit.dart deleted file mode 100644 index b8e3b997c..000000000 --- a/lib/worldstate/cubits/deals/darvodeal_cubit.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; - -import 'package:hydrated_bloc/hydrated_bloc.dart'; -import 'package:navis/utils/utils.dart'; -import 'package:navis/worldstate/cubits/deals/darvodeal_state.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:warframestat_client/warframestat_client.dart'; -import 'package:warframestat_repository/warframestat_repository.dart'; - -class DarvodealCubit extends HydratedCubit { - DarvodealCubit(this.repository) : super(DarvodealInitial()); - - final WarframestatRepository repository; - - Future fetchDeal(String uniqueName, String name) async { - emit(DarvodealLoading()); - - try { - Item? info; - if (await ConnectionManager.hasInternetConnection) { - info = await repository.fetchDealInfo(uniqueName, name); - } - - info = await ConnectionManager.call( - () async => repository.fetchDealInfo(uniqueName, name), - ); - - if (info != null) { - emit(DarvoDealLoaded(info)); - return; - } - - emit(DarvoDealNoInfo()); - } catch (e) { - emit(DarvoDealNoInfo()); - } - } - - @override - DarvodealState fromJson(Map? json) { - Sentry.addBreadcrumb(Breadcrumb(message: 'DarvoDealCubit.fromJson')); - if (json == null) return DarvodealLoading(); - - try { - return DarvoDealLoaded(MinimalItem.fromJson(json)); - } catch (e, s) { - Sentry.captureException(e, stackTrace: s); - return DarvodealInitial(); - } - } - - @override - Map? toJson(DarvodealState state) { - Sentry.addBreadcrumb(Breadcrumb(message: 'DarvoDealCubit.toJson')); - if (state is DarvoDealLoaded) { - return state.item.toJson(); - } - - return null; - } -} diff --git a/lib/worldstate/cubits/deals/darvodeal_state.dart b/lib/worldstate/cubits/deals/darvodeal_state.dart deleted file mode 100644 index 56e3fe730..000000000 --- a/lib/worldstate/cubits/deals/darvodeal_state.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:warframestat_client/warframestat_client.dart'; - -abstract class DarvodealState extends Equatable { - const DarvodealState(); - - @override - List get props => []; -} - -class DarvodealInitial extends DarvodealState {} - -class DarvodealLoading extends DarvodealState {} - -class DarvoDealLoaded extends DarvodealState { - const DarvoDealLoaded(this.item); - - final Item item; - - @override - List get props => [item]; - - @override - String toString() { - return item.uniqueName; - } -} - -class DarvoDealNoInfo extends DarvodealState {} diff --git a/lib/worldstate/views/timers.dart b/lib/worldstate/views/timers.dart index b6a9a9d7c..077f59d07 100644 --- a/lib/worldstate/views/timers.dart +++ b/lib/worldstate/views/timers.dart @@ -44,7 +44,7 @@ class _MobileTimers extends StatelessWidget { const SteelPathCard(), if (worldstate?.activeAlerts ?? false) const AlertsCard(), const CycleCard(), - const DuviriCycle(), + const DuviriCircuit(), if (worldstate?.activeSales ?? false) const DarvoDealCard(), if (worldstate?.deepArchimedeaActive ?? false) const DeepArchimedeaCard(), diff --git a/lib/worldstate/widgets/timers/alerts_card.dart b/lib/worldstate/widgets/timers/alerts_card.dart index 5e90ddedf..0139c3c50 100644 --- a/lib/worldstate/widgets/timers/alerts_card.dart +++ b/lib/worldstate/widgets/timers/alerts_card.dart @@ -1,28 +1,43 @@ +import 'package:black_hole_flutter/black_hole_flutter.dart'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:navis/codex/codex.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'; +import 'package:warframestat_repository/warframestat_repository.dart'; class AlertsCard extends StatelessWidget { const AlertsCard({super.key}); @override Widget build(BuildContext context) { - return AppCard( - child: BlocBuilder( - builder: (context, state) { - final alerts = switch (state) { - WorldstateSuccess() => state.worldstate.alerts, - _ => [], - }; - - return Column( - children: alerts.map((a) => _AlertWidget(alert: a)).toList(), - ); - }, - ), + final wsRepo = RepositoryProvider.of(context); + + return BlocBuilder( + builder: (context, state) { + final alerts = switch (state) { + WorldstateSuccess() => state.worldstate.alerts, + _ => [], + }; + + return Column( + children: alerts.map((a) { + return AppCard( + child: BlocProvider( + create: (_) => ItemCubit( + a.mission.reward!.countedItems[0].type, + wsRepo, + )..fetchByName(), + child: _AlertWidget(alert: a), + ), + ); + }).toList(), + ); + }, ); } } @@ -34,12 +49,13 @@ class _AlertWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; final mission = alert.mission; final node = mission.node; final type = mission.type; final faction = mission.faction; + final reward = mission.reward; + final enemyLvlRange = context.l10n .levelInfo(mission.minEnemyLevel ?? 0, mission.maxEnemyLevel ?? 0); @@ -48,37 +64,88 @@ class _AlertWidget extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RowItem( - icons: [ - // Since nightmare alerts aren't visible in the worldstate there - // is no need for a nightmare icon for alerts. - if (mission.archwingRequired ?? false) - const Icon( - WarframeSymbols.archwing, - color: Colors.blue, - size: 25, - ), - ], - text: Text(node), - child: - ColoredContainer.text(text: mission.reward?.itemString ?? ''), - ), - RowItem( - text: Text( - '$type ($faction) | $enemyLvlRange', - style: textTheme.bodySmall, + ListTile( + title: Row( + children: [ + Text(node), + if (mission.archwingRequired ?? false) ...{ + SizedBoxSpacer.spacerWidth8, + const Icon( + WarframeSymbols.archwing, + color: Colors.blue, + size: 25, + ), + }, + ], ), - child: CountdownTimer( - // Will default to DateTime.now() under the hood. - // ignore: avoid-non-null-assertion + subtitle: Text('$type ($faction) | $enemyLvlRange'), + trailing: CountdownTimer( tooltip: context.l10n.countdownTooltip(alert.expiry), - // Will default to DateTime.now() under the hood. - // ignore: avoid-non-null-assertion expiry: alert.expiry, ), + dense: true, ), + _AlertReward(reward: reward), ], ), ); } } + +class _AlertReward extends StatelessWidget { + const _AlertReward({required this.reward}); + + final Reward? reward; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final item = + switch (state) { ItemFetchSuccess() => state.item, _ => null }; + + final credits = NumberFormat().format(reward?.credits ?? 0); + + final child = ListTile( + title: RichText( + text: TextSpan( + text: reward?.itemString, + children: [ + if (reward?.credits != null) + TextSpan( + text: ' + $credits credits', + style: context.textTheme.bodySmall?.copyWith( + color: context.theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + subtitle: item?.description != null + ? Text( + item!.description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ) + : null, + trailing: item != null + ? CachedNetworkImage( + imageUrl: item.imageUrl, + height: 100, + width: 60, + ) + : null, + isThreeLine: item != null, + dense: true, + ); + + if (item == null) return child; + + return EntryViewOpenContainer( + item: item as MinimalItem, + builder: (_, __) => child, + ); + }, + ); + } +} diff --git a/lib/worldstate/widgets/timers/darvo_deal_card.dart b/lib/worldstate/widgets/timers/darvo_deal_card.dart index 1ebbee2dc..7854d0d42 100644 --- a/lib/worldstate/widgets/timers/darvo_deal_card.dart +++ b/lib/worldstate/widgets/timers/darvo_deal_card.dart @@ -5,10 +5,10 @@ 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:navis/worldstate/cubits/darvodeal_cubit.dart'; import 'package:navis/worldstate/cubits/worldstate_cubit.dart'; import 'package:navis_ui/navis_ui.dart'; import 'package:warframestat_client/warframestat_client.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; class DarvoDealCard extends StatelessWidget { const DarvoDealCard({super.key}); @@ -29,6 +29,8 @@ class DarvoDealCard extends StatelessWidget { @override Widget build(BuildContext context) { + final repo = RepositoryProvider.of(context); + return BlocBuilder( buildWhen: _buildWhen, builder: (context, state) { @@ -37,13 +39,7 @@ class DarvoDealCard extends StatelessWidget { _ => null, }; - final int stock; - if (deal != null) { - stock = deal.total - deal.sold; - } else { - stock = 0; - } - + final stock = deal != null ? deal.total - deal.sold : 0; final inStock = stock != 0; final expiry = deal?.expiry ?? DateTime.now(); @@ -77,7 +73,11 @@ class DarvoDealCard extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 4), margin: const EdgeInsets.only(top: 10), ), - if (deal != null) _DealWidget(deal: deal), + if (deal != null) + BlocProvider( + create: (_) => ItemCubit(deal.item, repo)..fetchByName(), + child: _DealWidget(deal: deal), + ), ], ), ), @@ -88,53 +88,21 @@ class DarvoDealCard extends StatelessWidget { } } -class _DealWidget extends StatefulWidget { +class _DealWidget extends StatelessWidget { const _DealWidget({required this.deal}); final DailyDeal deal; - @override - State<_DealWidget> createState() => _DealWidgetState(); -} - -class _DealWidgetState extends State<_DealWidget> { - bool _buildWhen(DarvodealState previous, DarvodealState current) { - if (previous is! DarvoDealLoaded || current is! DarvoDealLoaded) { - // Return true so the UI knows it doesn't have any info. - return true; - } - - return previous.item.uniqueName != current.item.uniqueName; - } - - @override - void initState() { - super.initState(); - - BlocProvider.of(context) - .fetchDeal(widget.deal.uniqueName, widget.deal.item); - } - - @override - void didUpdateWidget(covariant _DealWidget oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.deal.uniqueName != widget.deal.uniqueName) { - BlocProvider.of(context) - .fetchDeal(widget.deal.uniqueName, widget.deal.item); - } - } - @override Widget build(BuildContext context) { - final deal = widget.deal; - - return BlocBuilder( - buildWhen: _buildWhen, + return BlocBuilder( builder: (context, state) { - final item = state is DarvoDealLoaded ? state.item : null; + final item = switch (state) { + ItemFetchSuccess() => state.item, + _ => null, + }; - return Row( + final row = Row( children: [ if (item != null) Padding( @@ -166,6 +134,13 @@ class _DealWidgetState extends State<_DealWidget> { ), ], ); + + if (item == null) return row; + + return EntryViewOpenContainer( + item: item as MinimalItem, + builder: (_, __) => row, + ); }, ); } diff --git a/lib/worldstate/widgets/timers/duviri_circuit.dart b/lib/worldstate/widgets/timers/duviri_circuit.dart new file mode 100644 index 000000000..e07fdae9a --- /dev/null +++ b/lib/worldstate/widgets/timers/duviri_circuit.dart @@ -0,0 +1,171 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:navis/codex/codex.dart'; +import 'package:navis/l10n/l10n.dart'; +import 'package:navis/worldstate/cubits/worldstate/worldstate_cubit.dart'; +import 'package:navis_ui/navis_ui.dart'; +import 'package:warframestat_client/warframestat_client.dart'; +import 'package:warframestat_repository/warframestat_repository.dart'; + +class DuviriCircuit extends StatelessWidget { + const DuviriCircuit({super.key}); + + bool _buildWhen(SolsystemState previous, SolsystemState current) { + if (previous is! WorldstateSuccess || current is! WorldstateSuccess) { + return false; + } + + final previousCycle = previous.worldstate.duviriCycle; + final nextCycle = current.worldstate.duviriCycle; + + return previousCycle.id != nextCycle.id; + } + + @override + Widget build(BuildContext context) { + return AppCard( + child: BlocBuilder( + buildWhen: _buildWhen, + builder: (context, state) { + final cycle = switch (state) { + WorldstateSuccess() => state.worldstate.duviriCycle, + _ => null + }; + + final choices = + cycle?.choices.map((c) => CircuitChoiceTile(choice: c)); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircuitResetTimer(expiry: cycle?.expiry ?? DateTime.now()), + ...?choices, + ], + ); + }, + ), + ); + } +} + +class CircuitResetTimer extends StatelessWidget { + const CircuitResetTimer({super.key, required this.expiry}); + + final DateTime expiry; + + DateTime _getNextMonday() { + final now = DateTime.timestamp(); + + var daysUntilNextMonday = (DateTime.monday - now.weekday + 7) % 7; + daysUntilNextMonday = daysUntilNextMonday == 0 ? 7 : daysUntilNextMonday; + + final nextMondayMidnight = DateTime.utc( + now.year, + now.month, + now.day + daysUntilNextMonday, + ); + + return nextMondayMidnight; + } + + @override + Widget build(BuildContext context) { + final date = MaterialLocalizations.of(context).formatFullDate(expiry); + + return ListTile( + title: Text(context.l10n.circuitResetTitle), + trailing: CountdownTimer(tooltip: date, expiry: _getNextMonday()), + ); + } +} + +class CircuitChoiceTile extends StatelessWidget { + const CircuitChoiceTile({super.key, required this.choice}); + + final Choice choice; + + @override + Widget build(BuildContext context) { + final repo = RepositoryProvider.of(context); + final isSteelPatch = choice.category == 'hard'; + + var category = toBeginningOfSentenceCase(choice.category); + if (isSteelPatch) category = context.l10n.steelPathTitle; + + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text(category), + ), + ...choice.choices.map((c) { + final name = isSteelPatch ? '$c Incarnon Genesis' : c; + + return BlocProvider( + create: (_) { + final cubit = ItemCubit(name, repo); + + isSteelPatch + ? cubit.fetchIncarnonGenesis() + : cubit.fetchByName(); + + return cubit; + }, + child: _CircuitPathTile(name: name), + ); + }), + ], + ), + ); + } +} + +class _CircuitPathTile extends StatelessWidget { + const _CircuitPathTile({required this.name}); + + final String name; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final item = switch (state) { + ItemFetchSuccess() => state.item as MinimalItem, + _ => null, + }; + + final icon = item != null + ? CircleAvatar( + foregroundImage: CachedNetworkImageProvider(item.imageUrl), + radius: 20, + ) + : null; + + final tile = ListTile( + contentPadding: + const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + leading: icon, + title: Text(item?.name ?? name), + subtitle: Text( + item?.description ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + isThreeLine: true, + dense: true, + ); + + if (item == null) return tile; + + return EntryViewOpenContainer( + item: item, + builder: (_, __) => tile, + ); + }, + ); + } +} diff --git a/lib/worldstate/widgets/timers/duviri_cycle.dart b/lib/worldstate/widgets/timers/duviri_cycle.dart deleted file mode 100644 index 844e8c4aa..000000000 --- a/lib/worldstate/widgets/timers/duviri_cycle.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; -import 'package:navis/l10n/l10n.dart'; -import 'package:navis/worldstate/cubits/worldstate/worldstate_cubit.dart'; -import 'package:navis_ui/navis_ui.dart'; -import 'package:warframestat_client/warframestat_client.dart'; - -class DuviriCycle extends StatelessWidget { - const DuviriCycle({super.key}); - - bool _buildWhen(SolsystemState previous, SolsystemState current) { - if (previous is! WorldstateSuccess || current is! WorldstateSuccess) { - return false; - } - - final previousCycle = previous.worldstate.duviriCycle; - final nextCycle = current.worldstate.duviriCycle; - - return previousCycle.id != nextCycle.id; - } - - @override - Widget build(BuildContext context) { - return AppCard( - child: BlocBuilder( - buildWhen: _buildWhen, - builder: (context, state) { - final cycle = switch (state) { - WorldstateSuccess() => state.worldstate.duviriCycle, - _ => null - }; - - final choices = - cycle?.choices.map((c) => DuviriChoiceTile(choice: c)); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - DuviriResetTimer(expiry: cycle?.expiry ?? DateTime.now()), - ...?choices, - ], - ); - }, - ), - ); - } -} - -class DuviriResetTimer extends StatelessWidget { - const DuviriResetTimer({super.key, required this.expiry}); - - final DateTime expiry; - - DateTime _getNextMonday() { - final now = DateTime.timestamp(); - - var daysUntilNextMonday = (DateTime.monday - now.weekday + 7) % 7; - daysUntilNextMonday = daysUntilNextMonday == 0 ? 7 : daysUntilNextMonday; - - final nextMondayMidnight = DateTime.utc( - now.year, - now.month, - now.day + daysUntilNextMonday, - ); - - return nextMondayMidnight; - } - - @override - Widget build(BuildContext context) { - final date = MaterialLocalizations.of(context).formatFullDate(expiry); - - return ListTile( - title: Text(context.l10n.circuitResetTitle), - trailing: CountdownTimer(tooltip: date, expiry: _getNextMonday()), - ); - } -} - -class DuviriChoiceTile extends StatelessWidget { - const DuviriChoiceTile({super.key, required this.choice}); - - final Choice choice; - - @override - Widget build(BuildContext context) { - final secondaryContainer = Theme.of(context).colorScheme.secondaryContainer; - - var category = toBeginningOfSentenceCase(choice.category); - if (category == 'Hard') category = context.l10n.steelPathTitle; - - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text(category), - ), - Wrap( - spacing: 8, - alignment: WrapAlignment.center, - children: choice.choices - .map( - (c) => Chip( - label: Text(c), - side: BorderSide(color: secondaryContainer), - backgroundColor: secondaryContainer, - ), - ) - .toList(), - ), - ], - ), - ); - } -} diff --git a/lib/worldstate/widgets/widgets.dart b/lib/worldstate/widgets/widgets.dart index ff8b29beb..a658376ce 100644 --- a/lib/worldstate/widgets/widgets.dart +++ b/lib/worldstate/widgets/widgets.dart @@ -19,7 +19,7 @@ export 'timers/archon_card.dart'; export 'timers/cycle_card.dart'; export 'timers/darvo_deal_card.dart'; export 'timers/deep_archimedea.dart'; -export 'timers/duviri_cycle.dart'; +export 'timers/duviri_circuit.dart'; export 'timers/event_card.dart'; export 'timers/outpost_card.dart'; export 'timers/rewards.dart'; diff --git a/lib/worldstate/worldstate.dart b/lib/worldstate/worldstate.dart index 18345a7f4..1181e367c 100644 --- a/lib/worldstate/worldstate.dart +++ b/lib/worldstate/worldstate.dart @@ -1,4 +1,3 @@ -export 'cubits/darvodeal_cubit.dart'; export 'cubits/fissure_filter.dart'; export 'cubits/worldstate_cubit.dart'; export 'views/bounties.dart'; diff --git a/packages/warframestat_repository/lib/src/cache_client.dart b/packages/warframestat_repository/lib/src/cache_client.dart index 9a1988302..2073cad7b 100644 --- a/packages/warframestat_repository/lib/src/cache_client.dart +++ b/packages/warframestat_repository/lib/src/cache_client.dart @@ -31,7 +31,7 @@ class CacheClient extends BaseClient { final now = DateTime.timestamp(); final cached = cacheBox.get(request.url.toString()); - if (cached != null && cached.isExpired) { + if (cached != null && !cached.isExpired) { return StreamedResponse(Stream.value(cached.data), 200); } diff --git a/packages/warframestat_repository/lib/src/repository.dart b/packages/warframestat_repository/lib/src/repository.dart index 78b674494..4897e2e8a 100644 --- a/packages/warframestat_repository/lib/src/repository.dart +++ b/packages/warframestat_repository/lib/src/repository.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:hive_ce/hive.dart'; import 'package:http/http.dart'; import 'package:warframestat_client/warframestat_client.dart'; @@ -70,32 +69,9 @@ class WarframestatRepository { return client.fetchTargets(); } - /// Fetch the [MinimalItem] info for whatever item Darvo is currently selling. - /// Will catch the item until the end of the day (UTC) - Future fetchDealInfo(String uniqueName, String name) async { - const cacheTime = Duration(minutes: 30); - final client = WarframeItemsClient( - client: await _cacheClient(cacheTime), - ua: userAgent, - language: language, - ); - - final results = await client.search(name); - - var item = results.firstWhereOrNull((i) => i.uniqueName == uniqueName); - if (item == null) { - final internalName = uniqueName.split('/').last; - item = results.firstWhereOrNull( - (i) => i.uniqueName.contains(internalName), - ); - } - - return item; - } - /// Search warframe-items Future> searchItems(String query) async { - const cacheTime = Duration(minutes: 5); + const cacheTime = Duration(days: 7); final client = WarframeItemsClient( client: await _cacheClient(cacheTime), ua: userAgent, diff --git a/packages/warframestat_repository/test/warframestat_repository_test.dart b/packages/warframestat_repository/test/warframestat_repository_test.dart index b87a5ea42..e77cefe28 100644 --- a/packages/warframestat_repository/test/warframestat_repository_test.dart +++ b/packages/warframestat_repository/test/warframestat_repository_test.dart @@ -73,48 +73,6 @@ void main() { }); }); - group('Deal info', () { - final fixture = Fixtures.itemsFixture; - final json = jsonEncode(fixture); - - test('fetch() => get item', () async { - when(() => client.send(any())) - .thenAnswer((_) async => fakeResponse(json)); - - final deal = await repository.fetchDealInfo( - '/Lotus/Powersuits/Dragon/ChromaPrime', - 'Chroma Prime', - ); - - expect(deal, isNotNull); - expect(deal!.uniqueName, '/Lotus/Powersuits/Dragon/ChromaPrime'); - expect(deal.name, 'Chroma Prime'); - verify(() => client.send(any())); - }); - - test('fetch() => get item from cache', () async { - when(() => client.send(any())) - .thenAnswer((_) async => fakeResponse(json)); - - await repository.fetchDealInfo( - '/Lotus/Powersuits/Dragon/ChromaPrime', - 'Chroma Prime', - ); - - clearInteractions(client); - - final deal = await repository.fetchDealInfo( - '/Lotus/Powersuits/Dragon/ChromaPrime', - 'Chroma Prime', - ); - - expect(deal, isNotNull); - expect(deal!.uniqueName, '/Lotus/Powersuits/Dragon/ChromaPrime'); - expect(deal.name, 'Chroma Prime'); - verifyNever(() => client.send(any())); - }); - }); - group('Item search', () { final fixture = Fixtures.itemsFixture; final items = toItems(fixture); diff --git a/pubspec.lock b/pubspec.lock index ba6eb6be6..ea26c972b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" + sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" url: "https://pub.dev" source: hosted - version: "1.3.44" + version: "1.3.46" _macros: dependency: transitive description: dart @@ -458,10 +458,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" + sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.8.0" firebase_core_platform_interface: dependency: transitive description: @@ -482,26 +482,26 @@ packages: dependency: transitive description: name: firebase_messaging - sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e + sha256: "4d0968ecb860d7baa15a6e2af3469ec5b0d959e51c59ce84a52b0f7632a4aa5a" url: "https://pub.dev" source: hosted - version: "15.1.3" + version: "15.1.5" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d + sha256: a2cb3e7d71d40b6612e2d4e0daa0ae759f6a9d07f693f904d14d22aadf70be10 url: "https://pub.dev" source: hosted - version: "4.5.46" + version: "4.5.48" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387 + sha256: "1554e190f0cd9d6fe59f61ae0275ac12006fdb78b07669f1a260d1a9e6de3a1f" url: "https://pub.dev" source: hosted - version: "3.9.2" + version: "3.9.4" fish_repository: dependency: "direct main" description: @@ -1544,10 +1544,10 @@ packages: dependency: "direct main" description: name: warframestat_client - sha256: dfc8f94ff39ec908cee4dfd3b2afe570b95ec692e4ded7d9fdd240816130cec0 + sha256: "2cdf016c7e6b98a4dd757b8d2778a6bad466e9ab7218dda9e333a33e1d70f9f7" url: "https://pub.dev" source: hosted - version: "3.10.1" + version: "3.11.0" warframestat_repository: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e2ec98706..4e844e11a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,7 +45,7 @@ dependencies: sentry_hive: ^8.9.0 sentry_logging: ^8.9.0 simple_icons: ^10.1.3 - warframestat_client: ^3.10.1 + warframestat_client: ^3.11.0 warframestat_repository: path: packages/warframestat_repository