Files
reader-app/lib/features/reader/providers/reader_provider.dart
T
virtus 2b8fa4ee57
Build Android APK / build-apk (push) Successful in 19m27s
Build Android AAB / build-aab (push) Successful in 12m5s
feat: Update app layout with MainAppHeader and enhance user settings interface
2026-04-23 03:09:24 +07:00

154 lines
4.8 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/models/chapter_model.dart';
import '../../../core/models/reading_settings.dart';
import '../../../core/network/providers.dart';
import '../../../core/storage/local_store.dart';
import '../../../core/storage/offline_cache.dart';
// ─── Chapter content ─────────────────────────────────────────────────────────
final chapterProvider =
FutureProvider.family<ChapterModel, String>((ref, chapterId) async {
final offlineCache = ref.read(offlineCacheProvider);
// Try network first, fall back to cache
try {
final client = ref.read(apiClientProvider);
final res = await client.dio.get('/api/chapters/$chapterId');
final chapter = ChapterModel.fromJson(res.data as Map<String, dynamic>);
// Cache for offline use (fire and forget)
unawaited(offlineCache.saveChapter(chapter));
return chapter;
} catch (_) {
debugPrint('[READER][CHAPTER][ERROR] Failed to load chapterId=$chapterId from network, trying cache');
final cached = await offlineCache.loadChapter(chapterId);
if (cached != null) return cached;
debugPrint('[READER][CHAPTER][ERROR] No cache for chapterId=$chapterId');
rethrow;
}
});
// ─── Reading progress ─────────────────────────────────────────────────────────
class ReadingProgress {
final String novelId;
final String chapterId;
final int chapterNumber;
final double scrollOffset;
const ReadingProgress({
required this.novelId,
required this.chapterId,
required this.chapterNumber,
required this.scrollOffset,
});
}
class ReaderNotifier extends StateNotifier<ReadingProgress?> {
final Ref _ref;
String? _novelId;
ReaderNotifier(this._ref) : super(null);
void open(String novelId, String chapterId, int chapterNumber) {
_novelId = novelId;
state = ReadingProgress(
novelId: novelId,
chapterId: chapterId,
chapterNumber: chapterNumber,
scrollOffset: 0,
);
}
void resetCurrentChapterProgress() {
if (state == null) return;
state = ReadingProgress(
novelId: state!.novelId,
chapterId: state!.chapterId,
chapterNumber: state!.chapterNumber,
scrollOffset: 0,
);
// Persist immediately so a freshly opened chapter always resumes at top.
unawaited(_persistProgress(state!.chapterId, state!.chapterNumber, 0));
}
void updateScroll(double offset) {
if (state == null) return;
state = ReadingProgress(
novelId: state!.novelId,
chapterId: state!.chapterId,
chapterNumber: state!.chapterNumber,
scrollOffset: offset,
);
_debounceUpdate(offset);
}
Future<void> _persistProgress(
String chapterId, int chapterNumber, double offset) async {
final localStore = _ref.read(localStoreProvider);
await localStore.saveProgress(_novelId!, chapterId, chapterNumber, offset);
// Also notify server (fire and forget)
try {
final client = _ref.read(apiClientProvider);
await client.dio.post('/api/user/reading-progress', data: {
'novelId': _novelId,
'chapterId': chapterId,
'chapterNumber': chapterNumber,
'progress': offset,
});
} catch (_) {}
}
Timer? _debounceTimer;
void _debounceUpdate(double offset) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(seconds: 3), () {
if (state != null) {
unawaited(_persistProgress(state!.chapterId, state!.chapterNumber, offset));
}
});
}
@override
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
}
final readerProvider =
StateNotifierProvider<ReaderNotifier, ReadingProgress?>((ref) {
return ReaderNotifier(ref);
});
// ─── Reading settings ─────────────────────────────────────────────────────────
class ReadingSettingsNotifier extends StateNotifier<ReadingSettings> {
final Ref _ref;
ReadingSettingsNotifier(this._ref) : super(const ReadingSettings()) {
_load();
}
Future<void> _load() async {
final localStore = _ref.read(localStoreProvider);
final saved = await localStore.loadReadingSettings();
if (saved != null) state = saved;
}
Future<void> update(ReadingSettings settings) async {
state = settings;
final localStore = _ref.read(localStoreProvider);
await localStore.saveReadingSettings(settings);
}
}
final readingSettingsProvider =
StateNotifierProvider<ReadingSettingsNotifier, ReadingSettings>((ref) {
return ReadingSettingsNotifier(ref);
});