feat: 42kit Nav

feat: Animation Optimization
This commit is contained in:
2025-05-04 14:07:56 +08:00
parent a2de310d84
commit 03c941c970
23 changed files with 5618 additions and 493 deletions

41
lib/ui/nav/nav_state.dart Normal file
View File

@@ -0,0 +1,41 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/udb.dart';
import 'package:starcitizen_doctor/data/nav_api_data.dart';
part 'nav_state.freezed.dart';
part 'nav_state.g.dart';
@freezed
class NavState with _$NavState {
const factory NavState({
List<NavApiDocsItemData>? items,
@Default("") String errorInfo,
}) = _NavState;
}
@riverpod
class Nav extends _$Nav {
bool _mounted = true;
@override
NavState build() {
state = NavState();
loadData(1);
ref.onDispose(() {
_mounted = false;
});
return state;
}
void loadData(int pageNo) async {
if (!_mounted) return;
try {
final r = await UDBNavApi.getNavItems(pageNo: pageNo);
state = state.copyWith(items: r.docs, errorInfo: "");
} catch (e) {
state = state.copyWith(errorInfo: e.toString());
}
}
}

View File

@@ -0,0 +1,173 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'nav_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$NavState {
List<NavApiDocsItemData>? get items => throw _privateConstructorUsedError;
String get errorInfo => throw _privateConstructorUsedError;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$NavStateCopyWith<NavState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $NavStateCopyWith<$Res> {
factory $NavStateCopyWith(NavState value, $Res Function(NavState) then) =
_$NavStateCopyWithImpl<$Res, NavState>;
@useResult
$Res call({List<NavApiDocsItemData>? items, String errorInfo});
}
/// @nodoc
class _$NavStateCopyWithImpl<$Res, $Val extends NavState>
implements $NavStateCopyWith<$Res> {
_$NavStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = freezed,
Object? errorInfo = null,
}) {
return _then(_value.copyWith(
items: freezed == items
? _value.items
: items // ignore: cast_nullable_to_non_nullable
as List<NavApiDocsItemData>?,
errorInfo: null == errorInfo
? _value.errorInfo
: errorInfo // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$NavStateImplCopyWith<$Res>
implements $NavStateCopyWith<$Res> {
factory _$$NavStateImplCopyWith(
_$NavStateImpl value, $Res Function(_$NavStateImpl) then) =
__$$NavStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<NavApiDocsItemData>? items, String errorInfo});
}
/// @nodoc
class __$$NavStateImplCopyWithImpl<$Res>
extends _$NavStateCopyWithImpl<$Res, _$NavStateImpl>
implements _$$NavStateImplCopyWith<$Res> {
__$$NavStateImplCopyWithImpl(
_$NavStateImpl _value, $Res Function(_$NavStateImpl) _then)
: super(_value, _then);
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = freezed,
Object? errorInfo = null,
}) {
return _then(_$NavStateImpl(
items: freezed == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<NavApiDocsItemData>?,
errorInfo: null == errorInfo
? _value.errorInfo
: errorInfo // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$NavStateImpl implements _NavState {
const _$NavStateImpl(
{final List<NavApiDocsItemData>? items, this.errorInfo = ""})
: _items = items;
final List<NavApiDocsItemData>? _items;
@override
List<NavApiDocsItemData>? get items {
final value = _items;
if (value == null) return null;
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@JsonKey()
final String errorInfo;
@override
String toString() {
return 'NavState(items: $items, errorInfo: $errorInfo)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NavStateImpl &&
const DeepCollectionEquality().equals(other._items, _items) &&
(identical(other.errorInfo, errorInfo) ||
other.errorInfo == errorInfo));
}
@override
int get hashCode => Object.hash(
runtimeType, const DeepCollectionEquality().hash(_items), errorInfo);
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$NavStateImplCopyWith<_$NavStateImpl> get copyWith =>
__$$NavStateImplCopyWithImpl<_$NavStateImpl>(this, _$identity);
}
abstract class _NavState implements NavState {
const factory _NavState(
{final List<NavApiDocsItemData>? items,
final String errorInfo}) = _$NavStateImpl;
@override
List<NavApiDocsItemData>? get items;
@override
String get errorInfo;
/// Create a copy of NavState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$NavStateImplCopyWith<_$NavStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'nav_state.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$navHash() => r'2019b3f675fbaec4be794049d900bf2dcc8d5e37';
/// See also [Nav].
@ProviderFor(Nav)
final navProvider = AutoDisposeNotifierProvider<Nav, NavState>.internal(
Nav.new,
name: r'navProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$navHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$Nav = AutoDisposeNotifier<NavState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

203
lib/ui/nav/nav_ui.dart Normal file
View File

@@ -0,0 +1,203 @@
import 'dart:ui';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart' show TapGestureRecognizer;
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/nav/nav_state.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart';
class NavUI extends HookConsumerWidget {
const NavUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Column(
children: [
Expanded(
child: buildBody(context, ref),
),
SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text.rich(
TextSpan(children: [
TextSpan(text: "*对应链接指向的服务由第三方提供,我们不对其做任何担保,请用户自行判断使用风险 | "),
TextSpan(text: "网站导航数据由"),
TextSpan(
text: " 42kit ",
style: TextStyle(
color: Colors.white,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrlString("https://42kit.citizenwiki.cn/nav");
},
),
TextSpan(text: "提供"),
]),
style: TextStyle(
fontSize: 13,
color: Colors.white.withValues(alpha: .6),
),
),
SizedBox(width: 12),
],
),
],
),
);
}
Widget buildBody(BuildContext context, WidgetRef ref) {
final data = ref.watch(navProvider);
if (data.errorInfo.isNotEmpty) {
return Center(
child: Text(
data.errorInfo,
style: TextStyle(color: Colors.red),
),
);
}
if (data.items == null) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ProgressRing(),
SizedBox(height: 12),
Text("正在获取数据..."),
],
));
}
return MasonryGridView.count(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
itemCount: data.items!.length,
padding: EdgeInsets.only(left: 12, right: 12, bottom: 12),
itemBuilder: (BuildContext context, int index) {
const itemHeight = 160.0;
final item = data.items![index];
final itemName = item.name;
final itemImage = item.image.url;
return GridItemAnimator(
index: index,
child: GestureDetector(
onTap: () {
launchUrlString(item.link);
},
child: Tilt(
shadowConfig: const ShadowConfig(maxIntensity: .3),
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: SizedBox(
height: itemHeight,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
clipBehavior: Clip.hardEdge,
child: Stack(
children: [
Center(
child: CacheNetImage(
height: itemHeight,
width: double.infinity,
url: itemImage,
fit: BoxFit.fitWidth,
),
),
Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: .55),
),
),
ClipRect(
clipBehavior: Clip.hardEdge,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15.0, sigmaY: 15.0),
blendMode: BlendMode.srcOver,
child: SizedBox(
width: double.infinity,
height: itemHeight,
),
),
),
Container(
decoration: BoxDecoration(),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CacheNetImage(
url: itemImage,
height: 48,
width: 48,
fit: BoxFit.cover,
),
),
SizedBox(width: 12),
Flexible(
child: Text(
itemName,
style: TextStyle(
fontSize: 18,
),
),
),
],
),
SizedBox(height: 12),
Expanded(
child: Text(
item.abstract_,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.white.withValues(alpha: .75)),
),
),
Row(
children: [
for (var value in item.tags)
Container(
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: .6),
borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 3),
margin: EdgeInsets.only(right: 6),
child: Text(
value.name,
style: TextStyle(
fontSize: 13,
color: Colors.white,
),
),
)
],
)
],
),
),
],
),
),
),
),
),
);
},
);
}
}