Tour and introduce

This commit is contained in:
LittleSheep 2025-05-20 01:51:24 +08:00
parent 9e609b8fe4
commit 1f2a5c107d
8 changed files with 410 additions and 67 deletions

View File

@ -146,7 +146,6 @@ class MessageRepository {
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final messages =

View File

@ -11,6 +11,7 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/models/post.dart';
import 'package:island/widgets/check_in.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/tour/tour.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
@ -27,7 +28,8 @@ class ExploreScreen extends ConsumerWidget {
final user = ref.watch(userInfoProvider);
final activitiesNotifier = ref.watch(activityListNotifierProvider.notifier);
return AppScaffold(
return TourTriggerWidget(
child: AppScaffold(
appBar: AppBar(title: const Text('explore').tr()),
floatingActionButton: FloatingActionButton(
heroTag: Key("explore-page-fab"),
@ -50,7 +52,8 @@ class ExploreScreen extends ConsumerWidget {
contentBuilder:
(data, widgetCount, endItemView) => CustomScrollView(
slivers: [
if (user.hasValue) SliverToBoxAdapter(child: CheckInWidget()),
if (user.hasValue)
SliverToBoxAdapter(child: CheckInWidget()),
SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
@ -97,6 +100,7 @@ class ExploreScreen extends ConsumerWidget {
),
),
),
),
);
}
}

View File

@ -7,7 +7,7 @@ part of 'notification.dart';
// **************************************************************************
String _$notificationUnreadCountNotifierHash() =>
r'ddec25e8e693b8feb800c085ef87d65f0d172341';
r'074143cf208a3afe1495be405198532a23ef77c8';
/// See also [NotificationUnreadCountNotifier].
@ProviderFor(NotificationUnreadCountNotifier)

68
lib/services/tour.dart Normal file
View File

@ -0,0 +1,68 @@
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/config.dart';
import 'package:island/widgets/tour/techincal_review_intro.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tour.g.dart';
part 'tour.freezed.dart';
const kAppTourStatusKey = "app_tour_statuses";
const List<Tour> kAllTours = [
Tour(id: 'technical_review_intro', isStartup: true),
];
@freezed
abstract class Tour with _$Tour {
const Tour._();
const factory Tour({required String id, required bool isStartup}) = _Tour;
Widget get widget => switch (id) {
'technical_review_intro' => const TechicalReviewIntroWidget(),
_ => throw UnimplementedError(),
};
}
@riverpod
class TourStatusNotifier extends _$TourStatusNotifier {
@override
Map<String, bool> build() {
final prefs = ref.watch(sharedPreferencesProvider);
final storedJson = prefs.getString(kAppTourStatusKey);
if (storedJson != null) {
try {
final Map<String, dynamic> stored = jsonDecode(storedJson);
return Map<String, bool>.from(stored);
} catch (_) {
return {for (final id in kAllTours.map((e) => e.id)) id: false};
}
}
return {for (final id in kAllTours.map((e) => e.id)) id: false};
}
bool isTourShown(String tourId) => state[tourId] ?? false;
Future<void> _saveState(Map<String, bool> newState) async {
state = newState;
final prefs = ref.read(sharedPreferencesProvider);
await prefs.setString(kAppTourStatusKey, jsonEncode(newState));
}
Future<Widget?> showTour(String tourId) async {
if (!isTourShown(tourId) || true) {
final newState = {...state, tourId: true};
await _saveState(newState);
return kAllTours.firstWhere((e) => e.id == tourId).widget;
}
return null;
}
Future<void> resetTours() async {
final newState = {for (final id in kAllTours.map((e) => e.id)) id: false};
await _saveState(newState);
}
}

View File

@ -0,0 +1,145 @@
// 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 'tour.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Tour {
String get id; bool get isStartup;
/// Create a copy of Tour
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$TourCopyWith<Tour> get copyWith => _$TourCopyWithImpl<Tour>(this as Tour, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Tour&&(identical(other.id, id) || other.id == id)&&(identical(other.isStartup, isStartup) || other.isStartup == isStartup));
}
@override
int get hashCode => Object.hash(runtimeType,id,isStartup);
@override
String toString() {
return 'Tour(id: $id, isStartup: $isStartup)';
}
}
/// @nodoc
abstract mixin class $TourCopyWith<$Res> {
factory $TourCopyWith(Tour value, $Res Function(Tour) _then) = _$TourCopyWithImpl;
@useResult
$Res call({
String id, bool isStartup
});
}
/// @nodoc
class _$TourCopyWithImpl<$Res>
implements $TourCopyWith<$Res> {
_$TourCopyWithImpl(this._self, this._then);
final Tour _self;
final $Res Function(Tour) _then;
/// Create a copy of Tour
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? isStartup = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,isStartup: null == isStartup ? _self.isStartup : isStartup // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _Tour extends Tour {
const _Tour({required this.id, required this.isStartup}): super._();
@override final String id;
@override final bool isStartup;
/// Create a copy of Tour
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$TourCopyWith<_Tour> get copyWith => __$TourCopyWithImpl<_Tour>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Tour&&(identical(other.id, id) || other.id == id)&&(identical(other.isStartup, isStartup) || other.isStartup == isStartup));
}
@override
int get hashCode => Object.hash(runtimeType,id,isStartup);
@override
String toString() {
return 'Tour(id: $id, isStartup: $isStartup)';
}
}
/// @nodoc
abstract mixin class _$TourCopyWith<$Res> implements $TourCopyWith<$Res> {
factory _$TourCopyWith(_Tour value, $Res Function(_Tour) _then) = __$TourCopyWithImpl;
@override @useResult
$Res call({
String id, bool isStartup
});
}
/// @nodoc
class __$TourCopyWithImpl<$Res>
implements _$TourCopyWith<$Res> {
__$TourCopyWithImpl(this._self, this._then);
final _Tour _self;
final $Res Function(_Tour) _then;
/// Create a copy of Tour
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? isStartup = null,}) {
return _then(_Tour(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,isStartup: null == isStartup ? _self.isStartup : isStartup // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

28
lib/services/tour.g.dart Normal file
View File

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tour.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$tourStatusNotifierHash() =>
r'040aac2d7cf6d14e539c1b04cf311421ee133ed3';
/// See also [TourStatusNotifier].
@ProviderFor(TourStatusNotifier)
final tourStatusNotifierProvider =
AutoDisposeNotifierProvider<TourStatusNotifier, Map<String, bool>>.internal(
TourStatusNotifier.new,
name: r'tourStatusNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tourStatusNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TourStatusNotifier = AutoDisposeNotifier<Map<String, bool>>;
// 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

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class TechicalReviewIntroWidget extends StatelessWidget {
const TechicalReviewIntroWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
'技术性预览',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('👋').fontSize(32),
Text('你好呀~').fontSize(24),
Text('欢迎来使用 Solar Network 3.0 的技术性预览版。'),
const Gap(24),
Text('技术性预览的初衷是让我们更顺滑的将 3.0 发布出来,帮助我们一点一点的迁移数据。'),
const Gap(24),
Text('同时,既然是测试版,肯定有一系列的 Bug 和问题,请多多包涵,也欢迎积极反馈到 GitHub 上。'),
Text('目前帐号数据已经迁移完毕,其他数据将在未来逐渐迁移。还请耐心等待,不要重复创建以免未来数据冲突。'),
const Gap(24),
Text('最后,感谢你愿意参与技术性预览,祝你使用愉快!'),
const Gap(16),
Text('关掉这个对话框就开始探索吧!').fontSize(11),
],
).padding(horizontal: 20, vertical: 24),
),
),
],
),
);
}
}

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/tour.dart';
const List<String> kStartTours = ['technical_review_intro'];
class TourTriggerWidget extends HookConsumerWidget {
final Widget child;
const TourTriggerWidget({super.key, required this.child});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tourStatus = ref.watch(tourStatusNotifierProvider.notifier);
useEffect(() {
Future(() async {
for (final tour in kStartTours) {
final widget = await tourStatus.showTour(tour);
if (widget != null) {
if (!context.mounted) return;
await showModalBottomSheet(
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (context) => widget,
);
}
}
});
return null;
}, [tourStatus]);
return child;
}
}