feat(settings): 收藏列表 + 最近呼叫全量实现(#42~#45)
## 收藏(Gitea #42~#45) - `FetchFavoritesRequest` / `DeleteFavoriteRequest`:ApiRequestable,对齐 iOS FavouriteService - `FetchFavoritesUseCase`:GET 分页拉取 → upsert FavoriteRepository - `DeleteFavoriteUseCase`:POST delete → 同步删本地 DB - `FavoritesViewModel`:分页/刷新/加载更多/删除,DB Stream 驱动 - `FavoritesPage`:列表 + RefreshIndicator + Dismissible 左滑删除 + 类型图标 + 空状态 - `AppRouteName.settingsFavorites` + 路由注册 + auth guard - `settings_page.dart` 收藏行 onTap 接入导航 ## 最近呼叫(框架,API 对接待续) - `CallLogRequest` / `FetchCallLogsUseCase` / `RecentCallsViewModel` - `RecentCallsPage`:双 Tab(全部/未接)+ _CallLogTile(图标/时长/时间) - `AppRouteName.settingsRecentCalls` + 路由注册 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:im_app/features/settings/di/settings_providers.dart';
|
||||
|
||||
/// 收藏列表 ViewModel 状态(Gitea issue #44)
|
||||
class FavoritesState {
|
||||
final bool isLoading;
|
||||
final bool isLoadingMore;
|
||||
final bool hasMore;
|
||||
final int page;
|
||||
final String? error;
|
||||
|
||||
const FavoritesState({
|
||||
this.isLoading = false,
|
||||
this.isLoadingMore = false,
|
||||
this.hasMore = true,
|
||||
this.page = 1,
|
||||
this.error,
|
||||
});
|
||||
|
||||
FavoritesState copyWith({
|
||||
bool? isLoading,
|
||||
bool? isLoadingMore,
|
||||
bool? hasMore,
|
||||
int? page,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return FavoritesState(
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
page: page ?? this.page,
|
||||
error: clearError ? null : error ?? this.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 收藏列表 ViewModel
|
||||
///
|
||||
/// - 初始化时自动拉取第 1 页
|
||||
/// - [refresh]:下拉刷新(reset page=1,清空 DB 中旧数据由 repository.watchAll 自动反映)
|
||||
/// - [loadMore]:加载更多(page+1)
|
||||
/// - [deleteItem]:删除单条收藏
|
||||
///
|
||||
/// UI 直接 `ref.watch(allFavoritesProvider)` 监听 DB Stream 获取列表,
|
||||
/// ViewModel state 只负责 isLoading / error / pagination。
|
||||
class FavoritesViewModel extends Notifier<FavoritesState> {
|
||||
@override
|
||||
FavoritesState build() {
|
||||
// 初始化时自动拉取
|
||||
Future.microtask(() => _fetchPage(1, isRefresh: true));
|
||||
return const FavoritesState(isLoading: true);
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int page, {bool isRefresh = false}) async {
|
||||
if (isRefresh) {
|
||||
state = state.copyWith(isLoading: true, clearError: true, page: 1);
|
||||
} else {
|
||||
state = state.copyWith(isLoadingMore: true, clearError: true);
|
||||
}
|
||||
|
||||
try {
|
||||
final count = await ref
|
||||
.read(fetchFavoritesUseCaseProvider)
|
||||
.execute(page: page);
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
isLoadingMore: false,
|
||||
hasMore: count > 0,
|
||||
page: page,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
isLoadingMore: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 下拉刷新
|
||||
Future<void> refresh() => _fetchPage(1, isRefresh: true);
|
||||
|
||||
/// 加载更多(只有 hasMore && !isLoadingMore 时才触发)
|
||||
Future<void> loadMore() {
|
||||
if (!state.hasMore || state.isLoadingMore) {
|
||||
return Future.value();
|
||||
}
|
||||
return _fetchPage(state.page + 1);
|
||||
}
|
||||
|
||||
/// 删除单条收藏
|
||||
Future<void> deleteItem(int id) async {
|
||||
try {
|
||||
await ref.read(deleteFavoriteUseCaseProvider).execute(id);
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: '删除失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:im_app/app/router/app_route_name.dart';
|
||||
import 'package:im_app/features/settings/di/settings_providers.dart';
|
||||
|
||||
/// 最近呼叫页状态
|
||||
class RecentCallsState {
|
||||
final int tabIndex;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const RecentCallsState({
|
||||
this.tabIndex = 0,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
RecentCallsState copyWith({
|
||||
int? tabIndex,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return RecentCallsState(
|
||||
tabIndex: tabIndex ?? this.tabIndex,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 最近呼叫页 ViewModel
|
||||
///
|
||||
/// ## 数据流
|
||||
/// ```
|
||||
/// RecentCallsPage (build)
|
||||
/// └─ ref.watch(recentCallsViewModelProvider) 读取 loading / error
|
||||
/// └─ ref.watch(allCallLogsProvider) DB Stream 实时通话记录
|
||||
/// → RecentCallsViewModel.loadCallLogs()
|
||||
/// → FetchCallLogsUseCase.execute() POST /app/api/call/records
|
||||
/// → CallLogRepository.insertOrReplace* → Drift DB → Stream 刷新
|
||||
/// ```
|
||||
class RecentCallsViewModel extends Notifier<RecentCallsState> {
|
||||
@override
|
||||
RecentCallsState build() {
|
||||
Future.microtask(_init);
|
||||
return const RecentCallsState();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await Future.wait([loadCallLogs(), markAllRead()]);
|
||||
}
|
||||
|
||||
// ── Tab ────────────────────────────────────────────────────────────────────
|
||||
|
||||
void setTab(int index) {
|
||||
state = state.copyWith(tabIndex: index);
|
||||
}
|
||||
|
||||
// ── 数据拉取 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> loadCallLogs() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
try {
|
||||
await ref.read(fetchCallLogsUseCaseProvider).execute();
|
||||
state = state.copyWith(isLoading: false);
|
||||
} catch (e) {
|
||||
debugPrint('[RecentCallsViewModel] loadCallLogs error: $e');
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// ── 标记已读 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> markAllRead() async {
|
||||
try {
|
||||
await ref.read(callLogRepositoryProvider).markAllAsRead();
|
||||
} catch (e) {
|
||||
debugPrint('[RecentCallsViewModel] markAllRead error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 导航 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
void navigateBack(BuildContext context) {
|
||||
if (context.canPop()) context.pop();
|
||||
}
|
||||
|
||||
static void pushFrom(BuildContext context) {
|
||||
context.push(AppRouteName.settingsRecentCalls.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// 最近呼叫页 ViewModel Provider
|
||||
final recentCallsViewModelProvider =
|
||||
NotifierProvider<RecentCallsViewModel, RecentCallsState>(
|
||||
RecentCallsViewModel.new,
|
||||
);
|
||||
@@ -141,6 +141,10 @@ class SettingsViewModel extends Notifier<SettingsState> {
|
||||
void navigateToAbout(BuildContext context) {
|
||||
context.push(AppRouteName.settingsAbout.path);
|
||||
}
|
||||
|
||||
void navigateToRecentCalls(BuildContext context) {
|
||||
context.push(AppRouteName.settingsRecentCalls.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// 我的页面 ViewModel Provider
|
||||
|
||||
Reference in New Issue
Block a user