✨ Tour and introduce
This commit is contained in:
parent
9e609b8fe4
commit
1f2a5c107d
@ -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 =
|
||||
|
@ -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 {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
68
lib/services/tour.dart
Normal 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);
|
||||
}
|
||||
}
|
145
lib/services/tour.freezed.dart
Normal file
145
lib/services/tour.freezed.dart
Normal 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
28
lib/services/tour.g.dart
Normal 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
|
63
lib/widgets/tour/techincal_review_intro.dart
Normal file
63
lib/widgets/tour/techincal_review_intro.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
36
lib/widgets/tour/tour.dart
Normal file
36
lib/widgets/tour/tour.dart
Normal 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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user