Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new explore page #414

Merged
merged 17 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
include: package:very_good_analysis/analysis_options.yaml

analyzer:
plugins:
- dart_code_metrics
exclude:
- lib/generated/l10n.dart
- lib/generated/intl/messages_*.dart
- lib/src/**/*g.dart
- lib/src/models/**/*.g.dart
- lib/firebase_options.dart

linter:
Expand Down
4 changes: 4 additions & 0 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:dynamic_color/dynamic_color.dart';
import 'package:feedback_sentry/feedback_sentry.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:navis/codex/codex.dart';
import 'package:navis/explore/views/fish_view.dart';
import 'package:navis/home/home.dart';
import 'package:navis/l10n/l10n.dart';
import 'package:navis/settings/settings.dart';
Expand Down Expand Up @@ -173,6 +175,8 @@ class _NavisAppState extends State<NavisApp> with WidgetsBindingObserver {
BountiesPage.route: (_) => const BountiesPage(),
BaroInventory.route: (_) => const BaroInventory(),
SynthTargetsView.route: (_) => const SynthTargetsView(),
'/codex': (_) => const CodexSearchPage(),
'/fish': (_) => const FishPage(),
},
supportedLocales: NavisLocalizations.supportedLocales,
locale: language,
Expand Down
2 changes: 1 addition & 1 deletion lib/codex/bloc/search_state.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:warframestat_client/warframestat_client.dart';

abstract class SearchState extends Equatable {
sealed class SearchState extends Equatable {
const SearchState();
}

Expand Down
55 changes: 55 additions & 0 deletions lib/codex/utils/debouncer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dart:async';

typedef Debounceable<S, T> = Future<S?> Function(T parameter);

/// Returns a new function that is a debounced version of the given function.
///
/// This means that the original function will be called only after no calls
/// have been made for the given Duration.
Debounceable<S, T> debounce<S, T>(Debounceable<S?, T> function) {
_Debouncer? debouncer;

return (T parameter) async {
if (debouncer != null && !debouncer!.isCompleted) {
debouncer!.cancel();
}

debouncer = _Debouncer(300);

try {
await debouncer!.future;
} on _CancelException {
return null;
}

return function(parameter);
};
}

// A wrapper around Timer used for debouncing.
class _Debouncer {
_Debouncer(this.milliseconds) {
_timer = Timer(Duration(milliseconds: milliseconds), _onComplete);
}

late final Timer _timer;
final int milliseconds;

final _completer = Completer<void>();

void _onComplete() => _completer.complete();

Future<void> get future => _completer.future;

bool get isCompleted => _completer.isCompleted;

void cancel() {
_timer.cancel();
_completer.completeError(const _CancelException());
}
}

// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
const _CancelException();
}
151 changes: 82 additions & 69 deletions lib/codex/views/codex_search_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,95 +4,108 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:matomo_tracker/matomo_tracker.dart';
import 'package:navis/codex/codex.dart';
import 'package:navis/l10n/l10n.dart';
import 'package:navis_ui/navis_ui.dart';
import 'package:worldstate_repository/worldstate_repository.dart';

class CodexSearchView extends StatelessWidget {
const CodexSearchView({super.key});
class CodexSearchPage extends StatelessWidget {
const CodexSearchPage({super.key});

@override
Widget build(BuildContext context) {
final repo = RepositoryProvider.of<WorldstateRepository>(context);

return TraceableWidget(
child: BlocProvider(
create: (_) =>
SearchBloc(RepositoryProvider.of<WorldstateRepository>(context)),
child: const _CodexSearch(),
child: Scaffold(
body: SafeArea(
child: BlocProvider(
create: (_) => SearchBloc(repo),
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context,
),
sliver: SliverAppBar(
clipBehavior: Clip.none,
shape: const StadiumBorder(),
scrolledUnderElevation: 0,
titleSpacing: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
floating: true,
forceElevated: innerBoxIsScrolled,
title: const CodexSearchBar(),
),
),
];
},
body: const CodexSearchView(),
),
),
),
),
);
}
}

class _CodexSearch extends StatelessWidget {
const _CodexSearch();

List<Widget> _headerSliverBuilder(BuildContext context) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: const SliverTopbar(
floating: true,
snap: true,
child: CodexTextEditior(),
),
),
];
}
class CodexSearchView extends StatelessWidget {
const CodexSearchView({super.key});

@override
Widget build(BuildContext context) {
const openContainerColor = Colors.transparent;
const openContainerElevation = 0.0;
const cacheExtent = 250.0;

final l10n = context.l10n;

return NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, _) => _headerSliverBuilder(context),
body: BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is CodexSuccessfulSearch && state.results.isNotEmpty) {
return ListView.builder(
itemCount: state.results.length,
cacheExtent: cacheExtent,
itemBuilder: (BuildContext context, int index) {
return OpenContainer(
openColor: openContainerColor,
openElevation: openContainerElevation,
closedColor: openContainerColor,
closedElevation: openContainerElevation,
transitionType: ContainerTransitionType.fadeThrough,
openBuilder: (_, __) {
return EntryView(item: state.results[index]);
},
closedBuilder: (_, __) {
return CodexResult(item: state.results[index]);
},
);
},
);
}
return BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is CodexSuccessfulSearch && state.results.isNotEmpty) {
return CustomScrollView(
key: const PageStorageKey<String>('codex_search'),
slivers: [
SliverOverlapInjector(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverList.builder(
itemCount: state.results.length,
itemBuilder: (BuildContext context, int index) {
return OpenContainer(
closedColor: Theme.of(context).colorScheme.background,
openColor: Theme.of(context).colorScheme.background,
openBuilder: (_, __) {
return EntryView(item: state.results[index]);
},
closedBuilder: (_, onTap) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CodexResult(
item: state.results[index],
showDescription: true,
onTap: onTap,
),
);
},
);
},
),
],
);
}

if (state is CodexSuccessfulSearch && state.results.isEmpty) {
return Center(child: Text(l10n.codexNoResults));
}
if (state is CodexSuccessfulSearch && state.results.isEmpty) {
return Center(child: Text(l10n.codexNoResults));
}

if (state is CodexSearchEmpty) {
return Center(
child: Text(
l10n.codexHint,
textAlign: TextAlign.center,
),
);
}
if (state is CodexSearchEmpty) {
return const SizedBox.shrink();
}

if (state is CodexSearchError) {
return Center(child: Text(state.message));
}
if (state is CodexSearchError) {
return Center(child: Text(state.message));
}

return const Center(child: CircularProgressIndicator());
},
),
return const Center(child: CircularProgressIndicator());
},
);
}
}
3 changes: 2 additions & 1 deletion lib/codex/widgets/codex_entry/components.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:navis/codex/codex.dart';
import 'package:navis/l10n/l10n.dart';
import 'package:navis_ui/navis_ui.dart';
import 'package:warframestat_client/warframestat_client.dart';

Expand All @@ -27,7 +28,7 @@ class ItemComponents extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
const CategoryTitle(title: 'Components'),
CategoryTitle(title: context.l10n.componentsTitle),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expand Down
3 changes: 2 additions & 1 deletion lib/codex/widgets/codex_entry/drops.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:navis/l10n/l10n.dart';
import 'package:navis_ui/navis_ui.dart';
import 'package:warframestat_client/warframestat_client.dart';

Expand Down Expand Up @@ -43,7 +44,7 @@ class _DropEntry extends StatelessWidget {

return ListTile(
title: Text(location),
subtitle: Text('Drop chance $dropChance%'),
subtitle: Text(context.l10n.dropChance(dropChance)),
);
}
}
Loading