feat: eink-mode #1
29 changed files with 532 additions and 297 deletions
|
|
@ -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
14
.woodpecker/scripts/pr-lint.sh
Executable 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
|
||||||
42
lib/helpers/eink_detection_helper.dart
Normal file
42
lib/helpers/eink_detection_helper.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
44
lib/providers/eink_mode_provider.dart
Normal file
44
lib/providers/eink_mode_provider.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/providers/eink_mode_provider.g.dart
Normal file
32
lib/providers/eink_mode_provider.g.dart
Normal 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
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
40
pubspec.lock
40
pubspec.lock
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue