feat: eink-mode #1

Merged
fmartingr merged 9 commits from eink-mode into master 2026-02-12 22:25:38 +01:00
29 changed files with 532 additions and 297 deletions

View file

@ -6,25 +6,7 @@ when:
event: pull_request event: pull_request
steps: steps:
# Step 1: Install dependencies and generate code
setup:
image: ghcr.io/cirruslabs/flutter:stable
commands:
- flutter pub get
- flutter pub run build_runner build --delete-conflicting-outputs
# Step 2: Lint the project
lint: lint:
image: ghcr.io/cirruslabs/flutter:stable image: debian:bookworm-slim
commands: commands:
- dart format --set-exit-if-changed . - bash .woodpecker/scripts/pr-lint.sh
depends_on:
- setup
# Step 3: Static analysis
analyze:
image: ghcr.io/cirruslabs/flutter:stable
commands:
- flutter analyze
depends_on:
- setup

14
.woodpecker/scripts/pr-lint.sh Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -eux
apt-get update -qq && apt-get install -y --no-install-recommends bash curl git unzip xz-utils ca-certificates > /dev/null
git config --global --add safe.directory '*'
curl -fsSL -o /tmp/flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.41.0-stable.tar.xz
tar xf /tmp/flutter.tar.xz -C /opt && rm /tmp/flutter.tar.xz
export PATH="/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:$PATH"
flutter pub get
dart format --set-exit-if-changed .
flutter pub run build_runner build --delete-conflicting-outputs
flutter analyze

View file

@ -0,0 +1,42 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
/// Detects whether the current device is a known e-ink device.
class EinkDetectionHelper {
static const _einkManufacturers = {
'onyx', // BOOX
'boyue', // Likebook
'amazon', // Kindle
'kobo',
'rakuten', // Kobo
'pocketbook',
'remarkable',
'tolino',
'bigme',
'dasung',
};
/// Hisense e-ink phone model prefixes (e.g. A5, A7, A9).
static const _hisenseEinkModels = {'a5', 'a7', 'a9'};
/// Returns `true` when the device is a known e-ink device.
/// On non-Android platforms this always returns `false`.
static Future<bool> isEinkDevice() async {
if (!Platform.isAndroid) return false;
final info = await DeviceInfoPlugin().androidInfo;
final manufacturer = info.manufacturer.toLowerCase();
final model = info.model.toLowerCase();
if (_einkManufacturers.contains(manufacturer)) return true;
// Hisense makes both LCD and e-ink phones match specific model lines.
if (manufacturer == 'hisense') {
for (final prefix in _hisenseEinkModels) {
if (model.contains(prefix)) return true;
}
}
return false;
}
}

View file

@ -1,5 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:worldhopper/models/enhanced_metadata.dart';
import 'package:worldhopper/models/opds_entry.dart'; import 'package:worldhopper/models/opds_entry.dart';
import 'package:worldhopper/models/opds_link.dart'; import 'package:worldhopper/models/opds_link.dart';

View file

@ -0,0 +1,44 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:worldhopper/helpers/eink_detection_helper.dart';
part 'eink_mode_provider.g.dart';
/// Key for storing e-ink mode preference in SharedPreferences
const String _einkModeKey = 'eink_mode';
/// Provider for e-ink mode state management.
///
/// On first launch (no stored preference), auto-detects whether the device is
/// a known e-ink device and persists the result. Subsequent launches use the
/// stored value so the user's explicit choice is always respected.
@riverpod
class EinkModeNotifier extends _$EinkModeNotifier {
@override
bool build() {
_loadEinkMode();
return false; // Default value until loaded
}
/// Load e-ink mode from SharedPreferences, auto-detecting on first launch.
Future<void> _loadEinkMode() async {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getBool(_einkModeKey);
if (stored != null) {
state = stored;
} else {
// First launch auto-detect
final detected = await EinkDetectionHelper.isEinkDevice();
state = detected;
await prefs.setBool(_einkModeKey, detected);
}
}
/// Set e-ink mode and persist to SharedPreferences.
Future<void> setEinkMode(bool enabled) async {
state = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_einkModeKey, enabled);
}
}

View file

@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'eink_mode_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$einkModeNotifierHash() => r'4a5aa5c980270db066864fe75ebdd2c015725cd6';
/// Provider for e-ink mode state management.
///
/// On first launch (no stored preference), auto-detects whether the device is
/// a known e-ink device and persists the result. Subsequent launches use the
/// stored value so the user's explicit choice is always respected.
///
/// Copied from [EinkModeNotifier].
@ProviderFor(EinkModeNotifier)
final einkModeNotifierProvider =
AutoDisposeNotifierProvider<EinkModeNotifier, bool>.internal(
EinkModeNotifier.new,
name: r'einkModeNotifierProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$einkModeNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$EinkModeNotifier = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -285,7 +285,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.secondary .secondary
.withOpacity(0.5), .withValues(alpha: 0.5),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@ -299,7 +299,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.7), .withValues(alpha: 0.7),
), ),
), ),
], ],

View file

@ -278,7 +278,7 @@ class _LibraryBrowserScreenState extends ConsumerState<LibraryBrowserScreen> {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.secondary .secondary
.withOpacity(0.5), .withValues(alpha: 0.5),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@ -292,7 +292,7 @@ class _LibraryBrowserScreenState extends ConsumerState<LibraryBrowserScreen> {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.7), .withValues(alpha: 0.7),
), ),
), ),
], ],

View file

@ -122,7 +122,7 @@ class LibraryScreen extends ConsumerWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -163,7 +163,7 @@ class LibraryScreen extends ConsumerWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),

View file

@ -68,7 +68,8 @@ class PublicationDetailScreen extends ConsumerWidget {
false; false;
return AppBar( return AppBar(
backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.75), backgroundColor:
Theme.of(context).colorScheme.surface.withValues(alpha: 0.75),
title: Text(entry.title), title: Text(entry.title),
actions: isOffline actions: isOffline
? [ ? [
@ -94,7 +95,7 @@ class PublicationDetailScreen extends ConsumerWidget {
child: Icon( child: Icon(
Icons.book, Icons.book,
size: 100, size: 100,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
), ),
); );
} }
@ -467,8 +468,4 @@ class PublicationDetailScreen extends ConsumerWidget {
return _formatDate(dateTime); return _formatDate(dateTime);
} }
} }
String _stripHtml(String html) {
return html.replaceAll(RegExp(r'<[^>]*>'), '').trim();
}
} }

View file

@ -6,8 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_epub_viewer/flutter_epub_viewer.dart'; import 'package:flutter_epub_viewer/flutter_epub_viewer.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worldhopper/config/constants.dart';
import 'package:worldhopper/helpers/snackbar_helper.dart'; import 'package:worldhopper/helpers/snackbar_helper.dart';
import 'package:worldhopper/providers/eink_mode_provider.dart';
import 'package:worldhopper/models/enhanced_metadata.dart'; import 'package:worldhopper/models/enhanced_metadata.dart';
import 'package:worldhopper/models/opds_entry.dart'; import 'package:worldhopper/models/opds_entry.dart';
import 'package:worldhopper/models/reading_progress.dart'; import 'package:worldhopper/models/reading_progress.dart';
@ -355,7 +355,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
child: Text( child: Text(
'${(_loadingProgress * 100).toInt()}%', '${(_loadingProgress * 100).toInt()}%',
style: TextStyle( style: TextStyle(
color: Colors.white.withOpacity(0.7), color: Colors.white.withValues(alpha: 0.7),
fontSize: 14, fontSize: 14,
), ),
), ),
@ -402,7 +402,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
return PopScope( return PopScope(
canPop: true, canPop: true,
onPopInvoked: (bool didPop) { onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) { if (didPop) {
if (_reachedEndThisSession) { if (_reachedEndThisSession) {
_deleteCompletedProgress(); _deleteCompletedProgress();
@ -491,6 +491,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
feedUrl: widget.feedUrl, feedUrl: widget.feedUrl,
entry: widget.entry, entry: widget.entry,
isVisible: _reachedEndThisSession, isVisible: _reachedEndThisSession,
einkMode: ref.watch(einkModeNotifierProvider),
onBeforeNavigate: _deleteCompletedProgress, onBeforeNavigate: _deleteCompletedProgress,
), ),
], ],
@ -619,7 +620,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
/// Helper method to convert Flutter Color to CSS hex string /// Helper method to convert Flutter Color to CSS hex string
String _colorToHex(Color color) { String _colorToHex(Color color) {
return '#${color.value.toRadixString(16).substring(2).toUpperCase()}'; return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
} }
/// Toggle app bar visibility /// Toggle app bar visibility
@ -671,7 +672,9 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
right: 0, right: 0,
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: _showAppBar ? 1.0 : 0.0, opacity: _showAppBar ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200), duration: ref.watch(einkModeNotifierProvider)
? Duration.zero
: const Duration(milliseconds: 200),
child: Container( child: Container(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: topPadding, top: topPadding,
@ -683,8 +686,8 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Colors.black.withOpacity(0.7), Colors.black.withValues(alpha: 0.7),
Colors.black.withOpacity(0.0), Colors.black.withValues(alpha: 0.0),
], ],
stops: const [0.0, 1.0], stops: const [0.0, 1.0],
), ),

View file

@ -18,6 +18,7 @@ import 'package:worldhopper/providers/connectivity_provider.dart';
import 'package:worldhopper/providers/filter_quality_provider.dart'; import 'package:worldhopper/providers/filter_quality_provider.dart';
import 'package:worldhopper/providers/precache_pages_provider.dart'; import 'package:worldhopper/providers/precache_pages_provider.dart';
import 'package:worldhopper/providers/reading_mode_provider.dart'; import 'package:worldhopper/providers/reading_mode_provider.dart';
import 'package:worldhopper/providers/eink_mode_provider.dart';
import 'package:worldhopper/providers/two_page_mode_provider.dart'; import 'package:worldhopper/providers/two_page_mode_provider.dart';
import 'package:worldhopper/screens/reader/page_grouping.dart'; import 'package:worldhopper/screens/reader/page_grouping.dart';
import 'package:worldhopper/screens/reader/two_page_reader.dart'; import 'package:worldhopper/screens/reader/two_page_reader.dart';
@ -58,6 +59,9 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
List<PageSpread> _spreads = []; List<PageSpread> _spreads = [];
bool _wasTwoPage = false; bool _wasTwoPage = false;
// E-ink swipe tracking
double? _einkSwipeStartX;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -123,7 +127,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
return PopScope( return PopScope(
canPop: true, canPop: true,
onPopInvoked: (bool didPop) { onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) { if (didPop) {
if (_reachedEndThisSession) { if (_reachedEndThisSession) {
_deleteCompletedProgress(); _deleteCompletedProgress();
@ -255,6 +259,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
feedUrl: widget.feedUrl, feedUrl: widget.feedUrl,
entry: widget.entry, entry: widget.entry,
isVisible: _reachedEndThisSession, isVisible: _reachedEndThisSession,
einkMode: ref.watch(einkModeNotifierProvider),
onBeforeNavigate: _deleteCompletedProgress, onBeforeNavigate: _deleteCompletedProgress,
), ),
], ],
@ -266,15 +271,18 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
Widget _buildPagedReader(OPDSStreamLink streamLink, dynamic server, Widget _buildPagedReader(OPDSStreamLink streamLink, dynamic server,
{required bool reverse}) { {required bool reverse}) {
final filterQuality = ref.watch(filterQualityNotifierProvider); final filterQuality = ref.watch(filterQualityNotifierProvider);
final einkMode = ref.watch(einkModeNotifierProvider);
return GestureDetector( Widget gallery = GestureDetector(
onTap: () { onTap: () {
setState(() { setState(() {
_showControls = !_showControls; _showControls = !_showControls;
}); });
}, },
child: PhotoViewGallery.builder( child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(), scrollPhysics: einkMode
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
reverse: reverse, reverse: reverse,
builder: (context, index) { builder: (context, index) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
@ -315,6 +323,17 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
}, },
), ),
); );
if (einkMode) {
gallery = _wrapWithEinkSwipe(
child: gallery,
controller: _pageController,
itemCount: streamLink.pageCount,
reverse: reverse,
);
}
return gallery;
} }
Widget _buildTwoPageReader(OPDSStreamLink streamLink, dynamic server, Widget _buildTwoPageReader(OPDSStreamLink streamLink, dynamic server,
@ -342,13 +361,16 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
final authHeaders = AuthService().getAuthHeaders(server); final authHeaders = AuthService().getAuthHeaders(server);
return TwoPageReader( final einkMode = ref.watch(einkModeNotifierProvider);
Widget reader = TwoPageReader(
spreads: _spreads, spreads: _spreads,
pageController: _twoPageController!, pageController: _twoPageController!,
getPageUrl: (index) => streamLink.getPageUrl(index), getPageUrl: (index) => streamLink.getPageUrl(index),
headers: authHeaders, headers: authHeaders,
filterQuality: filterQuality, filterQuality: filterQuality,
reverse: rtl, reverse: rtl,
einkMode: einkMode,
onTap: () { onTap: () {
setState(() { setState(() {
_showControls = !_showControls; _showControls = !_showControls;
@ -368,6 +390,17 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
_precacheUpcomingPages(lastPage); _precacheUpcomingPages(lastPage);
}, },
); );
if (einkMode) {
reader = _wrapWithEinkSwipe(
child: reader,
controller: _twoPageController!,
itemCount: _spreads.length,
reverse: rtl,
);
}
return reader;
} }
Widget _buildVerticalReader(OPDSStreamLink streamLink, dynamic server) { Widget _buildVerticalReader(OPDSStreamLink streamLink, dynamic server) {
@ -438,6 +471,36 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
); );
} }
/// Wraps [child] with a [Listener] that detects horizontal swipes and calls
/// [jumpToPage] on [controller], giving instant page turns with no animation.
Widget _wrapWithEinkSwipe({
required Widget child,
required PageController controller,
required int itemCount,
required bool reverse,
}) {
return Listener(
onPointerDown: (e) => _einkSwipeStartX = e.position.dx,
onPointerUp: (e) {
if (_einkSwipeStartX == null) return;
final dx = e.position.dx - _einkSwipeStartX!;
_einkSwipeStartX = null;
if (dx.abs() < 50) return; // below swipe threshold
// Swipe left (dx < 0) next page, swipe right (dx > 0) previous
final direction = dx < 0 ? 1 : -1;
final effectiveDirection = reverse ? -direction : direction;
final currentIdx = controller.page?.round() ?? 0;
final target = currentIdx + effectiveDirection;
if (target >= 0 && target < itemCount) {
controller.jumpToPage(target);
}
},
onPointerCancel: (_) => _einkSwipeStartX = null,
child: child,
);
}
Widget _buildTopBar(BuildContext context, OPDSStreamLink streamLink) { Widget _buildTopBar(BuildContext context, OPDSStreamLink streamLink) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -445,7 +508,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
Colors.black.withOpacity(0.7), Colors.black.withValues(alpha: 0.7),
Colors.transparent, Colors.transparent,
], ],
), ),
@ -492,7 +555,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
Text( Text(
widget.entry.authors.join(', '), widget.entry.authors.join(', '),
style: TextStyle( style: TextStyle(
color: Colors.white.withOpacity(0.7), color: Colors.white.withValues(alpha: 0.7),
fontSize: 12, fontSize: 12,
), ),
maxLines: 1, maxLines: 1,
@ -649,6 +712,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
} }
Widget _buildBottomBar(BuildContext context, OPDSStreamLink streamLink) { Widget _buildBottomBar(BuildContext context, OPDSStreamLink streamLink) {
final einkMode = ref.watch(einkModeNotifierProvider);
final progress = (_currentPage + 1) / streamLink.pageCount; final progress = (_currentPage + 1) / streamLink.pageCount;
final globalMode = ref.watch(readingModeNotifierProvider); final globalMode = ref.watch(readingModeNotifierProvider);
final seriesOverride = widget.feedUrl != null final seriesOverride = widget.feedUrl != null
@ -701,7 +765,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
begin: Alignment.bottomCenter, begin: Alignment.bottomCenter,
end: Alignment.topCenter, end: Alignment.topCenter,
colors: [ colors: [
Colors.black.withOpacity(0.7), Colors.black.withValues(alpha: 0.7),
Colors.transparent, Colors.transparent,
], ],
), ),
@ -741,7 +805,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
// Progress bar // Progress bar
LinearProgressIndicator( LinearProgressIndicator(
value: progress, value: progress,
backgroundColor: Colors.white.withOpacity(0.3), backgroundColor: Colors.white.withValues(alpha: 0.3),
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white), valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
minHeight: 4, minHeight: 4,
), ),
@ -764,10 +828,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
icon: const Icon(Icons.chevron_right, icon: const Icon(Icons.chevron_right,
color: Colors.white), color: Colors.white),
onPressed: currentIdx > 0 onPressed: currentIdx > 0
? () => activeController.previousPage( ? () => einkMode
duration: const Duration(milliseconds: 300), ? activeController.jumpToPage(currentIdx - 1)
curve: Curves.easeInOut, : activeController.previousPage(
) duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
)
: null, : null,
iconSize: 32, iconSize: 32,
), ),
@ -775,10 +841,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
icon: icon:
const Icon(Icons.chevron_left, color: Colors.white), const Icon(Icons.chevron_left, color: Colors.white),
onPressed: currentIdx < totalItems - 1 onPressed: currentIdx < totalItems - 1
? () => activeController.nextPage( ? () => einkMode
duration: const Duration(milliseconds: 300), ? activeController.jumpToPage(currentIdx + 1)
curve: Curves.easeInOut, : activeController.nextPage(
) duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
)
: null, : null,
iconSize: 32, iconSize: 32,
), ),
@ -802,10 +870,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
icon: icon:
const Icon(Icons.chevron_left, color: Colors.white), const Icon(Icons.chevron_left, color: Colors.white),
onPressed: currentIdx > 0 onPressed: currentIdx > 0
? () => activeController.previousPage( ? () => einkMode
duration: const Duration(milliseconds: 300), ? activeController.jumpToPage(currentIdx - 1)
curve: Curves.easeInOut, : activeController.previousPage(
) duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
)
: null, : null,
iconSize: 32, iconSize: 32,
), ),
@ -813,10 +883,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
icon: const Icon(Icons.chevron_right, icon: const Icon(Icons.chevron_right,
color: Colors.white), color: Colors.white),
onPressed: currentIdx < totalItems - 1 onPressed: currentIdx < totalItems - 1
? () => activeController.nextPage( ? () => einkMode
duration: const Duration(milliseconds: 300), ? activeController.jumpToPage(currentIdx + 1)
curve: Curves.easeInOut, : activeController.nextPage(
) duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
)
: null, : null,
iconSize: 32, iconSize: 32,
), ),

View file

@ -14,6 +14,7 @@ class TwoPageReader extends StatelessWidget {
final Map<String, String> headers; final Map<String, String> headers;
final FilterQuality filterQuality; final FilterQuality filterQuality;
final bool reverse; final bool reverse;
final bool einkMode;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final VoidCallback onTap; final VoidCallback onTap;
@ -25,6 +26,7 @@ class TwoPageReader extends StatelessWidget {
required this.headers, required this.headers,
required this.filterQuality, required this.filterQuality,
required this.reverse, required this.reverse,
this.einkMode = false,
required this.onPageChanged, required this.onPageChanged,
required this.onTap, required this.onTap,
}); });
@ -38,7 +40,9 @@ class TwoPageReader extends StatelessWidget {
reverse: reverse, reverse: reverse,
itemCount: spreads.length, itemCount: spreads.length,
onPageChanged: onPageChanged, onPageChanged: onPageChanged,
physics: const BouncingScrollPhysics(), physics: einkMode
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
itemBuilder: (context, spreadIndex) { itemBuilder: (context, spreadIndex) {
final spread = spreads[spreadIndex]; final spread = spreads[spreadIndex];

View file

@ -251,8 +251,10 @@ class _EditServerScreenState extends ConsumerState<EditServerScreen> {
Text( Text(
label, label,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: color: Theme.of(context)
Theme.of(context).colorScheme.onSurface.withOpacity(0.6), .colorScheme
.onSurface
.withValues(alpha: 0.6),
), ),
), ),
Text( Text(

View file

@ -115,7 +115,8 @@ class ServerListScreen extends ConsumerWidget {
Icon( Icon(
Icons.cloud_off, Icons.cloud_off,
size: 100, size: 100,
color: Theme.of(context).colorScheme.secondary.withOpacity(0.5), color:
Theme.of(context).colorScheme.secondary.withValues(alpha: 0.5),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
@ -132,7 +133,7 @@ class ServerListScreen extends ConsumerWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.7), .withValues(alpha: 0.7),
), ),
), ),
), ),

View file

@ -20,72 +20,56 @@ class AppearanceSettingsScreen extends ConsumerWidget {
_buildSectionHeader(context, 'Theme'), _buildSectionHeader(context, 'Theme'),
Card( Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column( child: RadioGroup<ThemeMode>(
children: [ groupValue: themeMode,
ListTile( onChanged: (value) {
leading: const Icon(Icons.light_mode), if (value != null) {
title: const Text('Light'), ref.read(themeNotifierProvider.notifier).setThemeMode(value);
trailing: Radio<ThemeMode>( }
value: ThemeMode.light, },
groupValue: themeMode, child: Column(
onChanged: (value) { children: [
if (value != null) { ListTile(
ref leading: const Icon(Icons.light_mode),
.read(themeNotifierProvider.notifier) title: const Text('Light'),
.setThemeMode(value); trailing: const Radio<ThemeMode>(
} value: ThemeMode.light,
),
onTap: () {
ref
.read(themeNotifierProvider.notifier)
.setThemeMode(ThemeMode.light);
}, },
), ),
onTap: () { const Divider(height: 1),
ref ListTile(
.read(themeNotifierProvider.notifier) leading: const Icon(Icons.dark_mode),
.setThemeMode(ThemeMode.light); title: const Text('Dark'),
}, trailing: const Radio<ThemeMode>(
), value: ThemeMode.dark,
const Divider(height: 1), ),
ListTile( onTap: () {
leading: const Icon(Icons.dark_mode), ref
title: const Text('Dark'), .read(themeNotifierProvider.notifier)
trailing: Radio<ThemeMode>( .setThemeMode(ThemeMode.dark);
value: ThemeMode.dark,
groupValue: themeMode,
onChanged: (value) {
if (value != null) {
ref
.read(themeNotifierProvider.notifier)
.setThemeMode(value);
}
}, },
), ),
onTap: () { const Divider(height: 1),
ref ListTile(
.read(themeNotifierProvider.notifier) leading: const Icon(Icons.brightness_auto),
.setThemeMode(ThemeMode.dark); title: const Text('System'),
}, subtitle: const Text('Follow device theme'),
), trailing: const Radio<ThemeMode>(
const Divider(height: 1), value: ThemeMode.system,
ListTile( ),
leading: const Icon(Icons.brightness_auto), onTap: () {
title: const Text('System'), ref
subtitle: const Text('Follow device theme'), .read(themeNotifierProvider.notifier)
trailing: Radio<ThemeMode>( .setThemeMode(ThemeMode.system);
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (value) {
if (value != null) {
ref
.read(themeNotifierProvider.notifier)
.setThemeMode(value);
}
}, },
), ),
onTap: () { ],
ref ),
.read(themeNotifierProvider.notifier)
.setThemeMode(ThemeMode.system);
},
),
],
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View file

@ -20,34 +20,36 @@ class FilterQualitySettingsScreen extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Card( Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column( child: RadioGroup<FilterQuality>(
children: [ groupValue: filterQuality,
for (final entry in _filterQualityOptions.entries) ...[ onChanged: (value) {
if (entry.key != _filterQualityOptions.keys.first) if (value != null) {
const Divider(height: 1), ref
ListTile( .read(filterQualityNotifierProvider.notifier)
leading: Icon(entry.value.icon), .setFilterQuality(value);
title: Text(entry.value.label), }
subtitle: Text(entry.value.description), },
trailing: Radio<FilterQuality>( child: Column(
value: entry.key, children: [
groupValue: filterQuality, for (final entry in _filterQualityOptions.entries) ...[
onChanged: (value) { if (entry.key != _filterQualityOptions.keys.first)
if (value != null) { const Divider(height: 1),
ref ListTile(
.read(filterQualityNotifierProvider.notifier) leading: Icon(entry.value.icon),
.setFilterQuality(value); title: Text(entry.value.label),
} subtitle: Text(entry.value.description),
trailing: Radio<FilterQuality>(
value: entry.key,
),
onTap: () {
ref
.read(filterQualityNotifierProvider.notifier)
.setFilterQuality(entry.key);
}, },
), ),
onTap: () { ],
ref
.read(filterQualityNotifierProvider.notifier)
.setFilterQuality(entry.key);
},
),
], ],
], ),
), ),
), ),
Padding( Padding(

View file

@ -20,34 +20,36 @@ class PrecachePagesSettingsScreen extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Card( Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column( child: RadioGroup<int>(
children: [ groupValue: precachePages,
for (final entry in _precachePagesOptions.entries) ...[ onChanged: (value) {
if (entry.key != _precachePagesOptions.keys.first) if (value != null) {
const Divider(height: 1), ref
ListTile( .read(precachePagesNotifierProvider.notifier)
leading: Icon(entry.value.icon), .setPrecachePages(value);
title: Text(entry.value.label), }
subtitle: Text(entry.value.description), },
trailing: Radio<int>( child: Column(
value: entry.key, children: [
groupValue: precachePages, for (final entry in _precachePagesOptions.entries) ...[
onChanged: (value) { if (entry.key != _precachePagesOptions.keys.first)
if (value != null) { const Divider(height: 1),
ref ListTile(
.read(precachePagesNotifierProvider.notifier) leading: Icon(entry.value.icon),
.setPrecachePages(value); title: Text(entry.value.label),
} subtitle: Text(entry.value.description),
trailing: Radio<int>(
value: entry.key,
),
onTap: () {
ref
.read(precachePagesNotifierProvider.notifier)
.setPrecachePages(entry.key);
}, },
), ),
onTap: () { ],
ref
.read(precachePagesNotifierProvider.notifier)
.setPrecachePages(entry.key);
},
),
], ],
], ),
), ),
), ),
Padding( Padding(

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worldhopper/providers/eink_mode_provider.dart';
import 'package:worldhopper/providers/filter_quality_provider.dart'; import 'package:worldhopper/providers/filter_quality_provider.dart';
import 'package:worldhopper/providers/precache_pages_provider.dart'; import 'package:worldhopper/providers/precache_pages_provider.dart';
import 'package:worldhopper/providers/reading_mode_provider.dart'; import 'package:worldhopper/providers/reading_mode_provider.dart';
@ -38,6 +39,7 @@ class ReadersSettingsScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final einkMode = ref.watch(einkModeNotifierProvider);
final readingMode = ref.watch(readingModeNotifierProvider); final readingMode = ref.watch(readingModeNotifierProvider);
final filterQuality = ref.watch(filterQualityNotifierProvider); final filterQuality = ref.watch(filterQualityNotifierProvider);
final precachePages = ref.watch(precachePagesNotifierProvider); final precachePages = ref.watch(precachePagesNotifierProvider);
@ -50,6 +52,25 @@ class ReadersSettingsScreen extends ConsumerWidget {
), ),
body: ListView( body: ListView(
children: [ children: [
_buildSectionHeader(context, 'General'),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
SwitchListTile(
secondary: const Icon(Icons.tablet_android),
title: const Text('E-ink mode'),
subtitle: const Text('Disable animations for e-ink displays'),
value: einkMode,
onChanged: (enabled) {
ref
.read(einkModeNotifierProvider.notifier)
.setEinkMode(enabled);
},
),
],
),
),
_buildSectionHeader(context, 'Image Reader'), _buildSectionHeader(context, 'Image Reader'),
Card( Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),

View file

@ -20,34 +20,36 @@ class ReadingModeSettingsScreen extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Card( Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column( child: RadioGroup<ReadingMode>(
children: [ groupValue: readingMode,
for (final entry in _readingModeOptions.entries) ...[ onChanged: (value) {
if (entry.key != _readingModeOptions.keys.first) if (value != null) {
const Divider(height: 1), ref
ListTile( .read(readingModeNotifierProvider.notifier)
leading: Icon(entry.value.icon), .setReadingMode(value);
title: Text(entry.value.label), }
subtitle: Text(entry.value.description), },
trailing: Radio<ReadingMode>( child: Column(
value: entry.key, children: [
groupValue: readingMode, for (final entry in _readingModeOptions.entries) ...[
onChanged: (value) { if (entry.key != _readingModeOptions.keys.first)
if (value != null) { const Divider(height: 1),
ref ListTile(
.read(readingModeNotifierProvider.notifier) leading: Icon(entry.value.icon),
.setReadingMode(value); title: Text(entry.value.label),
} subtitle: Text(entry.value.description),
trailing: Radio<ReadingMode>(
value: entry.key,
),
onTap: () {
ref
.read(readingModeNotifierProvider.notifier)
.setReadingMode(entry.key);
}, },
), ),
onTap: () { ],
ref
.read(readingModeNotifierProvider.notifier)
.setReadingMode(entry.key);
},
),
], ],
], ),
), ),
), ),
Padding( Padding(

View file

@ -30,7 +30,7 @@ class EpubDownloadService {
} }
final url = acquisitionLink.href; final url = acquisitionLink.href;
if (url == null || url.isEmpty) { if (url.isEmpty) {
throw Exception('Invalid acquisition link URL'); throw Exception('Invalid acquisition link URL');
} }
@ -76,7 +76,7 @@ class EpubDownloadService {
if (acquisitionLink == null) return null; if (acquisitionLink == null) return null;
final url = acquisitionLink.href; final url = acquisitionLink.href;
if (url == null || url.isEmpty) return null; if (url.isEmpty) return null;
final fileInfo = await _cacheManager.getFileFromCache(url); final fileInfo = await _cacheManager.getFileFromCache(url);
if (fileInfo != null && await fileInfo.file.exists()) { if (fileInfo != null && await fileInfo.file.exists()) {
@ -92,7 +92,7 @@ class EpubDownloadService {
if (acquisitionLink == null) return; if (acquisitionLink == null) return;
final url = acquisitionLink.href; final url = acquisitionLink.href;
if (url == null || url.isEmpty) return; if (url.isEmpty) return;
await _cacheManager.removeFile(url); await _cacheManager.removeFile(url);
} }
@ -103,7 +103,7 @@ class EpubDownloadService {
if (acquisitionLink == null) return false; if (acquisitionLink == null) return false;
final url = acquisitionLink.href; final url = acquisitionLink.href;
if (url == null || url.isEmpty) return false; if (url.isEmpty) return false;
final fileInfo = await _cacheManager.getFileFromCache(url); final fileInfo = await _cacheManager.getFileFromCache(url);
return fileInfo != null && await fileInfo.file.exists(); return fileInfo != null && await fileInfo.file.exists();
@ -118,7 +118,7 @@ class EpubDownloadService {
} }
final url = acquisitionLink.href; final url = acquisitionLink.href;
if (url == null || url.isEmpty) { if (url.isEmpty) {
throw Exception('Invalid acquisition link URL'); throw Exception('Invalid acquisition link URL');
} }

View file

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
/// Service for caching publication cover images locally /// Service for caching publication cover images locally
@ -36,7 +37,7 @@ class ImageCacheService {
return file.path; return file.path;
} catch (e) { } catch (e) {
print('Error caching cover: $e'); debugPrint('Error caching cover: $e');
return null; return null;
} }
} }
@ -59,7 +60,7 @@ class ImageCacheService {
return file.path; return file.path;
} catch (e) { } catch (e) {
print('Error caching thumbnail: $e'); debugPrint('Error caching thumbnail: $e');
return null; return null;
} }
} }

View file

@ -19,6 +19,7 @@ class NextInSeriesOverlay extends ConsumerStatefulWidget {
final String? feedUrl; final String? feedUrl;
final OPDSEntry entry; final OPDSEntry entry;
final bool isVisible; final bool isVisible;
final bool einkMode;
final Future<void> Function()? onBeforeNavigate; final Future<void> Function()? onBeforeNavigate;
const NextInSeriesOverlay({ const NextInSeriesOverlay({
@ -27,6 +28,7 @@ class NextInSeriesOverlay extends ConsumerStatefulWidget {
this.feedUrl, this.feedUrl,
required this.entry, required this.entry,
required this.isVisible, required this.isVisible,
this.einkMode = false,
this.onBeforeNavigate, this.onBeforeNavigate,
}); });
@ -124,82 +126,84 @@ class _NextInSeriesOverlayState extends ConsumerState<NextInSeriesOverlay> {
required String title, required String title,
required IconData trailingIcon, required IconData trailingIcon,
}) { }) {
final barContent = Material(
elevation: 8,
borderRadius: BorderRadius.circular(12),
color: color,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Padding(
padding:
const EdgeInsets.only(left: 16, top: 12, bottom: 12, right: 4),
child: Row(
children: [
Icon(icon, color: onColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: onColor.withValues(alpha: 0.7),
),
),
Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: onColor,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 4),
Icon(trailingIcon, color: onColor),
SizedBox(
width: 36,
height: 36,
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 20,
icon: Icon(
Icons.close,
color: onColor.withValues(alpha: 0.7),
),
onPressed: () {
setState(() {
_dismissed = true;
});
},
),
),
],
),
),
),
);
return Positioned( return Positioned(
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: MediaQuery.of(context).padding.bottom + 16,
left: 16, left: 16,
right: 16, right: 16,
child: Dismissible( child: widget.einkMode
key: ValueKey(key), ? barContent
direction: DismissDirection.down, : Dismissible(
onDismissed: (_) { key: ValueKey(key),
setState(() { direction: DismissDirection.down,
_dismissed = true; onDismissed: (_) {
}); setState(() {
}, _dismissed = true;
child: Material( });
elevation: 8, },
borderRadius: BorderRadius.circular(12), child: barContent,
color: color,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(
left: 16, top: 12, bottom: 12, right: 4),
child: Row(
children: [
Icon(icon, color: onColor),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label,
style:
Theme.of(context).textTheme.labelSmall?.copyWith(
color: onColor.withOpacity(0.7),
),
),
Text(
title,
style:
Theme.of(context).textTheme.titleSmall?.copyWith(
color: onColor,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 4),
Icon(trailingIcon, color: onColor),
SizedBox(
width: 36,
height: 36,
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 20,
icon: Icon(
Icons.close,
color: onColor.withOpacity(0.7),
),
onPressed: () {
setState(() {
_dismissed = true;
});
},
),
),
],
),
), ),
),
),
),
); );
} }

View file

@ -51,7 +51,7 @@ class PublicationCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -132,7 +132,7 @@ class PublicationCard extends StatelessWidget {
child: Icon( child: Icon(
entry.isNavigation ? Icons.folder : Icons.book, entry.isNavigation ? Icons.folder : Icons.book,
size: 48, size: 48,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3), color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
), ),
), ),
); );

View file

@ -126,7 +126,7 @@ class RecentlyReadCard extends ConsumerWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
), ),
], ],
@ -183,7 +183,7 @@ class RecentlyReadCard extends ConsumerWidget {
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.3), color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),

View file

@ -65,7 +65,7 @@ class ServerCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -123,7 +123,7 @@ class ServerCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
@ -132,7 +132,7 @@ class ServerCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
), ),
], ],
@ -145,7 +145,7 @@ class ServerCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
@ -154,7 +154,7 @@ class ServerCard extends StatelessWidget {
color: Theme.of(context) color: Theme.of(context)
.colorScheme .colorScheme
.onSurface .onSurface
.withOpacity(0.6), .withValues(alpha: 0.6),
), ),
), ),
], ],

View file

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import connectivity_plus import connectivity_plus
import device_info_plus
import flutter_inappwebview_macos import flutter_inappwebview_macos
import package_info_plus import package_info_plus
import shared_preferences_foundation import shared_preferences_foundation
@ -15,6 +16,7 @@ import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -149,10 +149,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.1"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@ -265,6 +265,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.11"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
url: "https://pub.dev"
source: hosted
version: "11.5.0"
device_info_plus_platform_interface:
dependency: transitive
description:
name: device_info_plus_platform_interface
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
url: "https://pub.dev"
source: hosted
version: "7.0.3"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
@ -588,18 +604,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.17" version: "0.12.18"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.13.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -1033,10 +1049,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.7" version: "0.7.8"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -1197,6 +1213,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.15.0" version: "5.15.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View file

@ -82,6 +82,7 @@ dependencies:
wakelock_plus: ^1.2.8 wakelock_plus: ^1.2.8
package_info_plus: ^9.0.0 package_info_plus: ^9.0.0
url_launcher: ^6.3.2 url_launcher: ^6.3.2
device_info_plus: ^11.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: