All checks were successful
ci/woodpecker/tag/release Pipeline was successful
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>
168 lines
6.8 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|