From 039f5b202faf11c26332a50a5fa1856de11228f8 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 8 May 2025 22:13:59 +0800 Subject: [PATCH] :recycle: Using activities based explore --- lib/models/activity.dart | 24 ++++ lib/models/activity.freezed.dart | 193 +++++++++++++++++++++++++++++++ lib/models/activity.g.dart | 34 ++++++ lib/screens/explore.dart | 92 +++++++++------ lib/widgets/check_in.dart | 11 ++ 5 files changed, 317 insertions(+), 37 deletions(-) create mode 100644 lib/models/activity.dart create mode 100644 lib/models/activity.freezed.dart create mode 100644 lib/models/activity.g.dart create mode 100644 lib/widgets/check_in.dart diff --git a/lib/models/activity.dart b/lib/models/activity.dart new file mode 100644 index 0000000..a98c6da --- /dev/null +++ b/lib/models/activity.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:island/models/user.dart'; + +part 'activity.freezed.dart'; +part 'activity.g.dart'; + +@freezed +abstract class SnActivity with _$SnActivity { + const factory SnActivity({ + required String id, + required String type, + required String resourceIdentifier, + required int visibility, + required int accountId, + required SnAccount account, + required dynamic data, + required DateTime createdAt, + required DateTime updatedAt, + required dynamic deletedAt, + }) = _SnActivity; + + factory SnActivity.fromJson(Map json) => + _$SnActivityFromJson(json); +} diff --git a/lib/models/activity.freezed.dart b/lib/models/activity.freezed.dart new file mode 100644 index 0000000..222b20f --- /dev/null +++ b/lib/models/activity.freezed.dart @@ -0,0 +1,193 @@ +// dart format width=80 +// 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 'activity.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$SnActivity { + + String get id; String get type; String get resourceIdentifier; int get visibility; int get accountId; SnAccount get account; dynamic get data; DateTime get createdAt; DateTime get updatedAt; dynamic get deletedAt; +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SnActivityCopyWith get copyWith => _$SnActivityCopyWithImpl(this as SnActivity, _$identity); + + /// Serializes this SnActivity to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.deletedAt, deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,visibility,accountId,account,const DeepCollectionEquality().hash(data),createdAt,updatedAt,const DeepCollectionEquality().hash(deletedAt)); + +@override +String toString() { + return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, visibility: $visibility, accountId: $accountId, account: $account, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class $SnActivityCopyWith<$Res> { + factory $SnActivityCopyWith(SnActivity value, $Res Function(SnActivity) _then) = _$SnActivityCopyWithImpl; +@useResult +$Res call({ + String id, String type, String resourceIdentifier, int visibility, int accountId, SnAccount account, dynamic data, DateTime createdAt, DateTime updatedAt, dynamic deletedAt +}); + + +$SnAccountCopyWith<$Res> get account; + +} +/// @nodoc +class _$SnActivityCopyWithImpl<$Res> + implements $SnActivityCopyWith<$Res> { + _$SnActivityCopyWithImpl(this._self, this._then); + + final SnActivity _self; + final $Res Function(SnActivity) _then; + +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? visibility = null,Object? accountId = null,Object? account = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable +as String,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable +as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable +as SnAccount,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as dynamic, + )); +} +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res> get account { + + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); +} +} + + +/// @nodoc +@JsonSerializable() + +class _SnActivity implements SnActivity { + const _SnActivity({required this.id, required this.type, required this.resourceIdentifier, required this.visibility, required this.accountId, required this.account, required this.data, required this.createdAt, required this.updatedAt, required this.deletedAt}); + factory _SnActivity.fromJson(Map json) => _$SnActivityFromJson(json); + +@override final String id; +@override final String type; +@override final String resourceIdentifier; +@override final int visibility; +@override final int accountId; +@override final SnAccount account; +@override final dynamic data; +@override final DateTime createdAt; +@override final DateTime updatedAt; +@override final dynamic deletedAt; + +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SnActivityCopyWith<_SnActivity> get copyWith => __$SnActivityCopyWithImpl<_SnActivity>(this, _$identity); + +@override +Map toJson() { + return _$SnActivityToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&const DeepCollectionEquality().equals(other.deletedAt, deletedAt)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,visibility,accountId,account,const DeepCollectionEquality().hash(data),createdAt,updatedAt,const DeepCollectionEquality().hash(deletedAt)); + +@override +String toString() { + return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, visibility: $visibility, accountId: $accountId, account: $account, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; +} + + +} + +/// @nodoc +abstract mixin class _$SnActivityCopyWith<$Res> implements $SnActivityCopyWith<$Res> { + factory _$SnActivityCopyWith(_SnActivity value, $Res Function(_SnActivity) _then) = __$SnActivityCopyWithImpl; +@override @useResult +$Res call({ + String id, String type, String resourceIdentifier, int visibility, int accountId, SnAccount account, dynamic data, DateTime createdAt, DateTime updatedAt, dynamic deletedAt +}); + + +@override $SnAccountCopyWith<$Res> get account; + +} +/// @nodoc +class __$SnActivityCopyWithImpl<$Res> + implements _$SnActivityCopyWith<$Res> { + __$SnActivityCopyWithImpl(this._self, this._then); + + final _SnActivity _self; + final $Res Function(_SnActivity) _then; + +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? visibility = null,Object? accountId = null,Object? account = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { + return _then(_SnActivity( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable +as String,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable +as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as int,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable +as SnAccount,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as dynamic,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable +as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable +as dynamic, + )); +} + +/// Create a copy of SnActivity +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnAccountCopyWith<$Res> get account { + + return $SnAccountCopyWith<$Res>(_self.account, (value) { + return _then(_self.copyWith(account: value)); + }); +} +} + +// dart format on diff --git a/lib/models/activity.g.dart b/lib/models/activity.g.dart new file mode 100644 index 0000000..021b402 --- /dev/null +++ b/lib/models/activity.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SnActivity _$SnActivityFromJson(Map json) => _SnActivity( + id: json['id'] as String, + type: json['type'] as String, + resourceIdentifier: json['resource_identifier'] as String, + visibility: (json['visibility'] as num).toInt(), + accountId: (json['account_id'] as num).toInt(), + account: SnAccount.fromJson(json['account'] as Map), + data: json['data'], + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'], +); + +Map _$SnActivityToJson(_SnActivity instance) => + { + 'id': instance.id, + 'type': instance.type, + 'resource_identifier': instance.resourceIdentifier, + 'visibility': instance.visibility, + 'account_id': instance.accountId, + 'account': instance.account.toJson(), + 'data': instance.data, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt, + }; diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index efe46cc..6241255 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gap/gap.dart'; +import 'package:island/models/activity.dart'; import 'package:island/route.gr.dart'; import 'package:island/widgets/app_scaffold.dart'; import 'package:island/models/post.dart'; @@ -16,8 +18,8 @@ class ExploreScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final posts = ref.watch(postListProvider); - final postsNotifier = ref.watch(postListProvider.notifier); + final posts = ref.watch(activityListProvider); + final postsNotifier = ref.watch(activityListProvider.notifier); return AppScaffold( appBar: AppBar(title: const Text('Explore')), @@ -26,7 +28,7 @@ class ExploreScreen extends ConsumerWidget { onPressed: () { context.router.push(PostComposeRoute()).then((value) { if (value != null) { - ref.invalidate(postListProvider); + ref.invalidate(activityListProvider); } }); }, @@ -34,45 +36,52 @@ class ExploreScreen extends ConsumerWidget { ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, body: RefreshIndicator( - onRefresh: - () => Future.sync((() { - ref.invalidate(postListProvider); - })), - child: InfiniteList( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - itemCount: posts.length, - isLoading: postsNotifier.isLoading, - hasReachedMax: postsNotifier.hasReachedMax, - onFetchData: postsNotifier.fetchMore, - itemBuilder: (context, index) { - final post = posts[index]; - return PostItem( - item: post, - onRefresh: (_) { - ref.invalidate(postListProvider); + onRefresh: () => postsNotifier.refresh(), + child: CustomScrollView( + slivers: [ + SliverInfiniteList( + itemCount: posts.length, + isLoading: postsNotifier.isLoading, + hasReachedMax: postsNotifier.hasReachedMax, + onFetchData: postsNotifier.fetchMore, + itemBuilder: (context, index) { + final item = posts[index]; + switch (item.type) { + case 'posts.new': + return PostItem( + item: SnPost.fromJson(item.data), + onRefresh: (_) { + ref.invalidate(activityListProvider); + }, + onUpdate: (post) { + postsNotifier.updateOne( + index, + item.copyWith(data: post.toJson()), + ); + }, + ); + default: + return Placeholder(); + } }, - onUpdate: (post) { - postsNotifier.updateOne(index, post); - }, - ); - }, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, __) => const Divider(height: 1), + ), + SliverGap(MediaQuery.of(context).padding.bottom + 16), + ], ), ), ); } } -final postListProvider = - StateNotifierProvider<_PostListController, List>((ref) { +final activityListProvider = + StateNotifierProvider<_ActivityListController, List>((ref) { final client = ref.watch(apiClientProvider); - return _PostListController(client); + return _ActivityListController(client); }); -class _PostListController extends StateNotifier> { - _PostListController(this._dio) : super([]); +class _ActivityListController extends StateNotifier> { + _ActivityListController(this._dio) : super([]); final Dio _dio; bool isLoading = false; @@ -87,16 +96,18 @@ class _PostListController extends StateNotifier> { try { final response = await _dio.get( - '/posts', + '/activities', queryParameters: {'offset': offset, 'take': take}, ); - final List fetched = + final List fetched = (response.data as List) - .map((e) => SnPost.fromJson(e as Map)) + .map((e) => SnActivity.fromJson(e as Map)) .toList(); - final headerTotal = int.tryParse(response.headers['x-total']?.first ?? ''); + final headerTotal = int.tryParse( + response.headers['x-total']?.first ?? '', + ); if (headerTotal != null) total = headerTotal; if (!mounted) return; // Check if the notifier is still mounted @@ -111,7 +122,14 @@ class _PostListController extends StateNotifier> { } } - void updateOne(int index, SnPost post) { + Future refresh() async { + offset = 0; + state = []; + hasReachedMax = false; + await fetchMore(); + } + + void updateOne(int index, SnActivity post) { if (!mounted) return; // Check if the notifier is still mounted final updatedPosts = [...state]; updatedPosts[index] = post; diff --git a/lib/widgets/check_in.dart b/lib/widgets/check_in.dart new file mode 100644 index 0000000..d6f7fad --- /dev/null +++ b/lib/widgets/check_in.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class CheckInWidget extends HookConsumerWidget { + const CheckInWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const Placeholder(); + } +}