fix:对比原型增加功能交互
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import '../models/models.dart';
|
||||
|
||||
class AuthState {
|
||||
const AuthState({this.loggedIn = false, this.pendingAction});
|
||||
|
||||
final bool loggedIn;
|
||||
final PendingLoginAction? pendingAction;
|
||||
|
||||
AuthState copyWith({bool? loggedIn, Object? pendingAction = _sentinel}) {
|
||||
return AuthState(
|
||||
loggedIn: loggedIn ?? this.loggedIn,
|
||||
pendingAction: identical(pendingAction, _sentinel)
|
||||
? this.pendingAction
|
||||
: pendingAction as PendingLoginAction?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PendingLoginAction {
|
||||
const PendingLoginAction({
|
||||
required this.action,
|
||||
required this.reportId,
|
||||
required this.contextText,
|
||||
});
|
||||
|
||||
final LoginRequiredAction action;
|
||||
final String reportId;
|
||||
final String contextText;
|
||||
}
|
||||
|
||||
enum LoginRequiredAction { favorite, saveListen }
|
||||
|
||||
enum LoginMethod { phone, wechat, apple }
|
||||
|
||||
class ProfileState {
|
||||
const ProfileState({
|
||||
this.favorites = const {},
|
||||
this.savedListens = const {},
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
final Set<String> favorites;
|
||||
final Set<String> savedListens;
|
||||
final List<String> history;
|
||||
|
||||
ProfileState copyWith({
|
||||
Set<String>? favorites,
|
||||
Set<String>? savedListens,
|
||||
List<String>? history,
|
||||
}) {
|
||||
return ProfileState(
|
||||
favorites: favorites ?? this.favorites,
|
||||
savedListens: savedListens ?? this.savedListens,
|
||||
history: history ?? this.history,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailNavigationState {
|
||||
const DetailNavigationState({
|
||||
this.originTab = AppTab.recommend,
|
||||
this.stack = const [],
|
||||
this.tabScroll = const {},
|
||||
});
|
||||
|
||||
final AppTab originTab;
|
||||
final List<DetailStackEntry> stack;
|
||||
final Map<AppTab, double> tabScroll;
|
||||
|
||||
DetailNavigationState copyWith({
|
||||
AppTab? originTab,
|
||||
List<DetailStackEntry>? stack,
|
||||
Map<AppTab, double>? tabScroll,
|
||||
}) {
|
||||
return DetailNavigationState(
|
||||
originTab: originTab ?? this.originTab,
|
||||
stack: stack ?? this.stack,
|
||||
tabScroll: tabScroll ?? this.tabScroll,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailStackEntry {
|
||||
const DetailStackEntry({
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.scrollTop = 0,
|
||||
});
|
||||
|
||||
final DetailEntryType type;
|
||||
final String id;
|
||||
final double scrollTop;
|
||||
|
||||
DetailStackEntry copyWith({double? scrollTop}) {
|
||||
return DetailStackEntry(
|
||||
type: type,
|
||||
id: id,
|
||||
scrollTop: scrollTop ?? this.scrollTop,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum DetailEntryType { report, institution }
|
||||
|
||||
enum AppTab { recommend, reports, institutions, listen, profile }
|
||||
|
||||
class SheetState {
|
||||
const SheetState.hidden() : intent = null;
|
||||
const SheetState.visible(this.intent);
|
||||
|
||||
final SheetIntent? intent;
|
||||
|
||||
bool get isVisible => intent != null;
|
||||
}
|
||||
|
||||
sealed class SheetIntent {
|
||||
const SheetIntent();
|
||||
}
|
||||
|
||||
class LoginSheetIntent extends SheetIntent {
|
||||
const LoginSheetIntent({required this.contextText});
|
||||
|
||||
final String contextText;
|
||||
}
|
||||
|
||||
class FilterSheetIntent extends SheetIntent {
|
||||
const FilterSheetIntent();
|
||||
}
|
||||
|
||||
class OutboundSheetIntent extends SheetIntent {
|
||||
const OutboundSheetIntent({required this.scene, this.refId, this.targetUrl});
|
||||
|
||||
final String scene;
|
||||
final String? refId;
|
||||
final String? targetUrl;
|
||||
}
|
||||
|
||||
class ProfileListSheetIntent extends SheetIntent {
|
||||
const ProfileListSheetIntent({
|
||||
required this.kind,
|
||||
required this.title,
|
||||
this.reports = const [],
|
||||
});
|
||||
|
||||
final ProfileListKind kind;
|
||||
final String title;
|
||||
final List<ReportCardModel> reports;
|
||||
}
|
||||
|
||||
enum ProfileListKind { favorites, history, saved }
|
||||
|
||||
class OutboundEvent {
|
||||
const OutboundEvent({required this.scene, this.refId, this.targetUrl});
|
||||
|
||||
final String scene;
|
||||
final String? refId;
|
||||
final String? targetUrl;
|
||||
}
|
||||
|
||||
const Object _sentinel = Object();
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../models/models.dart';
|
||||
import '../repositories/user_state_repository.dart';
|
||||
import 'app_interaction_state.dart';
|
||||
import 'report_query.dart';
|
||||
|
||||
class RecommendTopicController extends StateNotifier<String> {
|
||||
RecommendTopicController() : super('全部');
|
||||
|
||||
void select(String topic) {
|
||||
state = topic;
|
||||
}
|
||||
}
|
||||
|
||||
class ReportFilterController extends StateNotifier<ReportQuery> {
|
||||
ReportFilterController() : super(const ReportQuery());
|
||||
|
||||
void setSearch(String value) {
|
||||
state = state.copyWith(search: value);
|
||||
}
|
||||
|
||||
void setTopic(String? topic) {
|
||||
state = state.copyWith(topic: topic);
|
||||
}
|
||||
|
||||
void setInstitution(String? institutionId) {
|
||||
state = state.copyWith(institutionId: institutionId);
|
||||
}
|
||||
|
||||
void toggleAudio() {
|
||||
state = state.copyWith(hasAudio: !state.hasAudio);
|
||||
}
|
||||
|
||||
void setSort(ReportSort sort) {
|
||||
state = state.copyWith(sort: sort);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = const ReportQuery();
|
||||
}
|
||||
}
|
||||
|
||||
class AuthController extends StateNotifier<AuthState> {
|
||||
AuthController(this._repository) : super(const AuthState()) {
|
||||
_load();
|
||||
}
|
||||
|
||||
final UserStateRepository _repository;
|
||||
|
||||
Future<void> _load() async {
|
||||
state = state.copyWith(loggedIn: await _repository.isLoggedIn());
|
||||
}
|
||||
|
||||
void requireLogin(PendingLoginAction action) {
|
||||
if (state.loggedIn) return;
|
||||
state = state.copyWith(pendingAction: action);
|
||||
}
|
||||
|
||||
Future<PendingLoginAction?> login(LoginMethod method) async {
|
||||
final pending = state.pendingAction;
|
||||
await _repository.login(method);
|
||||
state = const AuthState(loggedIn: true);
|
||||
return pending;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await _repository.logout();
|
||||
state = const AuthState();
|
||||
}
|
||||
|
||||
void clearPending() {
|
||||
state = state.copyWith(pendingAction: null);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileController extends StateNotifier<ProfileState> {
|
||||
ProfileController(this._repository) : super(const ProfileState()) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
final UserStateRepository _repository;
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = ProfileState(
|
||||
favorites: await _repository.getFavorites(),
|
||||
savedListens: await _repository.getSavedListens(),
|
||||
history: await _repository.getHistory(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(String reportId) async {
|
||||
await _repository.toggleFavorite(reportId);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> toggleSavedListen(String reportId) async {
|
||||
await _repository.toggleSavedListen(reportId);
|
||||
await refresh();
|
||||
}
|
||||
|
||||
Future<void> addHistory(String reportId) async {
|
||||
await _repository.addHistory(reportId);
|
||||
await refresh();
|
||||
}
|
||||
}
|
||||
|
||||
class DetailNavigationController extends StateNotifier<DetailNavigationState> {
|
||||
DetailNavigationController() : super(const DetailNavigationState());
|
||||
|
||||
void rememberTabScroll(AppTab tab, double scrollTop) {
|
||||
state = state.copyWith(tabScroll: {...state.tabScroll, tab: scrollTop});
|
||||
}
|
||||
|
||||
void push(DetailStackEntry entry, {required AppTab originTab}) {
|
||||
final stack = [...state.stack, entry];
|
||||
state = state.copyWith(originTab: originTab, stack: stack);
|
||||
}
|
||||
|
||||
void updateCurrentScroll(double scrollTop) {
|
||||
if (state.stack.isEmpty) return;
|
||||
final stack = [...state.stack];
|
||||
stack[stack.length - 1] = stack.last.copyWith(scrollTop: scrollTop);
|
||||
state = state.copyWith(stack: stack);
|
||||
}
|
||||
|
||||
DetailStackEntry? pop() {
|
||||
if (state.stack.isEmpty) return null;
|
||||
final stack = [...state.stack]..removeLast();
|
||||
state = state.copyWith(stack: stack);
|
||||
return stack.isEmpty ? null : stack.last;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = const DetailNavigationState();
|
||||
}
|
||||
}
|
||||
|
||||
class SheetController extends StateNotifier<SheetState> {
|
||||
SheetController() : super(const SheetState.hidden());
|
||||
|
||||
void show(SheetIntent intent) {
|
||||
state = SheetState.visible(intent);
|
||||
}
|
||||
|
||||
void hide() {
|
||||
state = const SheetState.hidden();
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileListBuilder {
|
||||
const ProfileListBuilder(this.reports);
|
||||
|
||||
final List<ReportCardModel> reports;
|
||||
|
||||
List<ReportCardModel> byIds(Iterable<String> ids) {
|
||||
final byId = {for (final report in reports) report.id: report};
|
||||
return ids.map((id) => byId[id]).whereType<ReportCardModel>().toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
class ReportQuery {
|
||||
const ReportQuery({
|
||||
this.search = '',
|
||||
this.topic,
|
||||
this.institutionId,
|
||||
this.hasAudio = false,
|
||||
this.sort = ReportSort.latest,
|
||||
});
|
||||
|
||||
final String search;
|
||||
final String? topic;
|
||||
final String? institutionId;
|
||||
final bool hasAudio;
|
||||
final ReportSort sort;
|
||||
|
||||
bool get hasActiveFilter =>
|
||||
search.trim().isNotEmpty ||
|
||||
topic != null ||
|
||||
institutionId != null ||
|
||||
hasAudio ||
|
||||
sort != ReportSort.latest;
|
||||
|
||||
ReportQuery copyWith({
|
||||
String? search,
|
||||
Object? topic = _sentinel,
|
||||
Object? institutionId = _sentinel,
|
||||
bool? hasAudio,
|
||||
ReportSort? sort,
|
||||
}) {
|
||||
return ReportQuery(
|
||||
search: search ?? this.search,
|
||||
topic: identical(topic, _sentinel) ? this.topic : topic as String?,
|
||||
institutionId: identical(institutionId, _sentinel)
|
||||
? this.institutionId
|
||||
: institutionId as String?,
|
||||
hasAudio: hasAudio ?? this.hasAudio,
|
||||
sort: sort ?? this.sort,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ReportSort { latest, oldest }
|
||||
|
||||
const Object _sentinel = Object();
|
||||
Reference in New Issue
Block a user