worldhopper/lib/screens/settings/readers_settings_screen.dart
Felipe M. 545638c86d
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
feat: report reading progress to OPDS server on page turns
Fire a GET request to the PSE page URL on each page turn so servers like
Kavita can track reading progress even when images are served from the
local cache. Uses ResponseType.stream and drains the body immediately to
avoid holding the full image in memory. Gated behind a new "Sync progress
for cached pages" setting (off by default) since it consumes additional
data. On book completion the last page is reported so the server marks
the book as read (covers both image and EPUB readers).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:58:25 +01:00

168 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worldhopper/l10n/app_localizations.dart';
import 'package:worldhopper/helpers/l10n_helpers.dart';
import 'package:worldhopper/providers/eink_mode_provider.dart';
import 'package:worldhopper/providers/filter_quality_provider.dart';
import 'package:worldhopper/providers/precache_pages_provider.dart';
import 'package:worldhopper/providers/reading_mode_provider.dart';
import 'package:worldhopper/providers/cover_page_provider.dart';
import 'package:worldhopper/providers/sync_progress_provider.dart';
import 'package:worldhopper/providers/two_page_mode_provider.dart';
import 'package:worldhopper/screens/settings/filter_quality_settings_screen.dart';
import 'package:worldhopper/screens/settings/precache_pages_settings_screen.dart';
import 'package:worldhopper/screens/settings/reading_mode_settings_screen.dart';
import 'package:worldhopper/widgets/worldhopper_app_bar.dart';
/// Screen for reader-related settings
class ReadersSettingsScreen extends ConsumerWidget {
const ReadersSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final einkMode = ref.watch(einkModeNotifierProvider);
final readingMode = ref.watch(readingModeNotifierProvider);
final filterQuality = ref.watch(filterQualityNotifierProvider);
final precachePages = ref.watch(precachePagesNotifierProvider);
final twoPageMode = ref.watch(twoPageModeNotifierProvider);
final firstPageIsCover = ref.watch(coverPageNotifierProvider);
final syncProgress = ref.watch(syncProgressNotifierProvider);
return Scaffold(
appBar: WorldhopperAppBar(
title: Text(l10n.readersTitle),
),
body: ListView(
children: [
_buildSectionHeader(context, l10n.readersGeneralSection),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
SwitchListTile(
secondary: const Icon(Icons.tablet_android),
title: Text(l10n.readersEinkMode),
subtitle: Text(l10n.readersEinkModeSubtitle),
value: einkMode,
onChanged: (enabled) {
ref
.read(einkModeNotifierProvider.notifier)
.setEinkMode(enabled);
},
),
],
),
),
_buildSectionHeader(context, l10n.readersImageReaderSection),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.chrome_reader_mode_outlined),
title: Text(l10n.readersReadingDirection),
subtitle: Text(readingModeLabel(l10n, readingMode)),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ReadingModeSettingsScreen(),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.high_quality_outlined),
title: Text(l10n.readersFilterQuality),
subtitle: Text(filterQualityLabel(l10n, filterQuality)),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const FilterQualitySettingsScreen(),
),
);
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.auto_stories),
title: Text(l10n.readersTwoPageSpread),
subtitle: Text(l10n.readersTwoPageSpreadSubtitle),
value: twoPageMode == TwoPageMode.auto,
onChanged: (enabled) {
ref
.read(twoPageModeNotifierProvider.notifier)
.setTwoPageMode(
enabled ? TwoPageMode.auto : TwoPageMode.off);
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.looks_one),
title: Text(l10n.readersFirstPageIsCover),
subtitle: Text(l10n.readersFirstPageIsCoverSubtitle),
value: firstPageIsCover,
onChanged: (value) {
ref
.read(coverPageNotifierProvider.notifier)
.setCoverPage(value);
},
),
],
),
),
_buildSectionHeader(context, l10n.readersCachingSection),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
ListTile(
leading: const Icon(Icons.cached_outlined),
title: Text(l10n.readersPrecachePages),
subtitle: Text(precachePagesLabel(l10n, precachePages)),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const PrecachePagesSettingsScreen(),
),
);
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.sync),
title: Text(l10n.readersSyncProgress),
subtitle: Text(l10n.readersSyncProgressSubtitle),
value: syncProgress,
onChanged: (enabled) {
ref
.read(syncProgressNotifierProvider.notifier)
.setSyncProgress(enabled);
},
),
],
),
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(32, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
}