feat: eink-mode #1
29 changed files with 532 additions and 297 deletions
|
|
@ -6,25 +6,7 @@ when:
|
|||
event: pull_request
|
||||
|
||||
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:
|
||||
image: ghcr.io/cirruslabs/flutter:stable
|
||||
image: debian:bookworm-slim
|
||||
commands:
|
||||
- dart format --set-exit-if-changed .
|
||||
depends_on:
|
||||
- setup
|
||||
|
||||
# Step 3: Static analysis
|
||||
analyze:
|
||||
image: ghcr.io/cirruslabs/flutter:stable
|
||||
commands:
|
||||
- flutter analyze
|
||||
depends_on:
|
||||
- setup
|
||||
- bash .woodpecker/scripts/pr-lint.sh
|
||||
|
|
|
|||
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:worldhopper/models/enhanced_metadata.dart';
|
||||
import 'package:worldhopper/models/opds_entry.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)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(0.5),
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
|
|
@ -299,7 +299,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.7),
|
||||
.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class _LibraryBrowserScreenState extends ConsumerState<LibraryBrowserScreen> {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary
|
||||
.withOpacity(0.5),
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
|
|
@ -292,7 +292,7 @@ class _LibraryBrowserScreenState extends ConsumerState<LibraryBrowserScreen> {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.7),
|
||||
.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class LibraryScreen extends ConsumerWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -163,7 +163,7 @@ class LibraryScreen extends ConsumerWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ class PublicationDetailScreen extends ConsumerWidget {
|
|||
false;
|
||||
|
||||
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),
|
||||
actions: isOffline
|
||||
? [
|
||||
|
|
@ -94,7 +95,7 @@ class PublicationDetailScreen extends ConsumerWidget {
|
|||
child: Icon(
|
||||
Icons.book,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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:flutter_epub_viewer/flutter_epub_viewer.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worldhopper/config/constants.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/opds_entry.dart';
|
||||
import 'package:worldhopper/models/reading_progress.dart';
|
||||
|
|
@ -355,7 +355,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
child: Text(
|
||||
'${(_loadingProgress * 100).toInt()}%',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
|
|
@ -402,7 +402,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
if (didPop) {
|
||||
if (_reachedEndThisSession) {
|
||||
_deleteCompletedProgress();
|
||||
|
|
@ -491,6 +491,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
feedUrl: widget.feedUrl,
|
||||
entry: widget.entry,
|
||||
isVisible: _reachedEndThisSession,
|
||||
einkMode: ref.watch(einkModeNotifierProvider),
|
||||
onBeforeNavigate: _deleteCompletedProgress,
|
||||
),
|
||||
],
|
||||
|
|
@ -619,7 +620,7 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
|
||||
/// Helper method to convert Flutter Color to CSS hex string
|
||||
String _colorToHex(Color color) {
|
||||
return '#${color.value.toRadixString(16).substring(2).toUpperCase()}';
|
||||
return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}';
|
||||
}
|
||||
|
||||
/// Toggle app bar visibility
|
||||
|
|
@ -671,7 +672,9 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
right: 0,
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showAppBar ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
duration: ref.watch(einkModeNotifierProvider)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(
|
||||
top: topPadding,
|
||||
|
|
@ -683,8 +686,8 @@ class _EpubReaderScreenState extends ConsumerState<EpubReaderScreen> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.7),
|
||||
Colors.black.withOpacity(0.0),
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
Colors.black.withValues(alpha: 0.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/precache_pages_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/screens/reader/page_grouping.dart';
|
||||
import 'package:worldhopper/screens/reader/two_page_reader.dart';
|
||||
|
|
@ -58,6 +59,9 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
List<PageSpread> _spreads = [];
|
||||
bool _wasTwoPage = false;
|
||||
|
||||
// E-ink swipe tracking
|
||||
double? _einkSwipeStartX;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -123,7 +127,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
onPopInvoked: (bool didPop) {
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) {
|
||||
if (didPop) {
|
||||
if (_reachedEndThisSession) {
|
||||
_deleteCompletedProgress();
|
||||
|
|
@ -255,6 +259,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
feedUrl: widget.feedUrl,
|
||||
entry: widget.entry,
|
||||
isVisible: _reachedEndThisSession,
|
||||
einkMode: ref.watch(einkModeNotifierProvider),
|
||||
onBeforeNavigate: _deleteCompletedProgress,
|
||||
),
|
||||
],
|
||||
|
|
@ -266,15 +271,18 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
Widget _buildPagedReader(OPDSStreamLink streamLink, dynamic server,
|
||||
{required bool reverse}) {
|
||||
final filterQuality = ref.watch(filterQualityNotifierProvider);
|
||||
final einkMode = ref.watch(einkModeNotifierProvider);
|
||||
|
||||
return GestureDetector(
|
||||
Widget gallery = GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showControls = !_showControls;
|
||||
});
|
||||
},
|
||||
child: PhotoViewGallery.builder(
|
||||
scrollPhysics: const BouncingScrollPhysics(),
|
||||
scrollPhysics: einkMode
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
reverse: reverse,
|
||||
builder: (context, index) {
|
||||
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,
|
||||
|
|
@ -342,13 +361,16 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
|
||||
final authHeaders = AuthService().getAuthHeaders(server);
|
||||
|
||||
return TwoPageReader(
|
||||
final einkMode = ref.watch(einkModeNotifierProvider);
|
||||
|
||||
Widget reader = TwoPageReader(
|
||||
spreads: _spreads,
|
||||
pageController: _twoPageController!,
|
||||
getPageUrl: (index) => streamLink.getPageUrl(index),
|
||||
headers: authHeaders,
|
||||
filterQuality: filterQuality,
|
||||
reverse: rtl,
|
||||
einkMode: einkMode,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_showControls = !_showControls;
|
||||
|
|
@ -368,6 +390,17 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
_precacheUpcomingPages(lastPage);
|
||||
},
|
||||
);
|
||||
|
||||
if (einkMode) {
|
||||
reader = _wrapWithEinkSwipe(
|
||||
child: reader,
|
||||
controller: _twoPageController!,
|
||||
itemCount: _spreads.length,
|
||||
reverse: rtl,
|
||||
);
|
||||
}
|
||||
|
||||
return reader;
|
||||
}
|
||||
|
||||
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) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -445,7 +508,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.7),
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
|
|
@ -492,7 +555,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
Text(
|
||||
widget.entry.authors.join(', '),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
fontSize: 12,
|
||||
),
|
||||
maxLines: 1,
|
||||
|
|
@ -649,6 +712,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
}
|
||||
|
||||
Widget _buildBottomBar(BuildContext context, OPDSStreamLink streamLink) {
|
||||
final einkMode = ref.watch(einkModeNotifierProvider);
|
||||
final progress = (_currentPage + 1) / streamLink.pageCount;
|
||||
final globalMode = ref.watch(readingModeNotifierProvider);
|
||||
final seriesOverride = widget.feedUrl != null
|
||||
|
|
@ -701,7 +765,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black.withOpacity(0.7),
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
|
|
@ -741,7 +805,7 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
// Progress bar
|
||||
LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: Colors.white.withOpacity(0.3),
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.3),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
minHeight: 4,
|
||||
),
|
||||
|
|
@ -764,10 +828,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
icon: const Icon(Icons.chevron_right,
|
||||
color: Colors.white),
|
||||
onPressed: currentIdx > 0
|
||||
? () => activeController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
? () => einkMode
|
||||
? activeController.jumpToPage(currentIdx - 1)
|
||||
: activeController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
: null,
|
||||
iconSize: 32,
|
||||
),
|
||||
|
|
@ -775,10 +841,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
icon:
|
||||
const Icon(Icons.chevron_left, color: Colors.white),
|
||||
onPressed: currentIdx < totalItems - 1
|
||||
? () => activeController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
? () => einkMode
|
||||
? activeController.jumpToPage(currentIdx + 1)
|
||||
: activeController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
: null,
|
||||
iconSize: 32,
|
||||
),
|
||||
|
|
@ -802,10 +870,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
icon:
|
||||
const Icon(Icons.chevron_left, color: Colors.white),
|
||||
onPressed: currentIdx > 0
|
||||
? () => activeController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
? () => einkMode
|
||||
? activeController.jumpToPage(currentIdx - 1)
|
||||
: activeController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
: null,
|
||||
iconSize: 32,
|
||||
),
|
||||
|
|
@ -813,10 +883,12 @@ class _ReaderScreenState extends ConsumerState<ReaderScreen> {
|
|||
icon: const Icon(Icons.chevron_right,
|
||||
color: Colors.white),
|
||||
onPressed: currentIdx < totalItems - 1
|
||||
? () => activeController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
? () => einkMode
|
||||
? activeController.jumpToPage(currentIdx + 1)
|
||||
: activeController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
: null,
|
||||
iconSize: 32,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class TwoPageReader extends StatelessWidget {
|
|||
final Map<String, String> headers;
|
||||
final FilterQuality filterQuality;
|
||||
final bool reverse;
|
||||
final bool einkMode;
|
||||
final ValueChanged<int> onPageChanged;
|
||||
final VoidCallback onTap;
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ class TwoPageReader extends StatelessWidget {
|
|||
required this.headers,
|
||||
required this.filterQuality,
|
||||
required this.reverse,
|
||||
this.einkMode = false,
|
||||
required this.onPageChanged,
|
||||
required this.onTap,
|
||||
});
|
||||
|
|
@ -38,7 +40,9 @@ class TwoPageReader extends StatelessWidget {
|
|||
reverse: reverse,
|
||||
itemCount: spreads.length,
|
||||
onPageChanged: onPageChanged,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: einkMode
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, spreadIndex) {
|
||||
final spread = spreads[spreadIndex];
|
||||
|
||||
|
|
|
|||
|
|
@ -251,8 +251,10 @@ class _EditServerScreenState extends ConsumerState<EditServerScreen> {
|
|||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
|
|
|
|||
|
|
@ -115,7 +115,8 @@ class ServerListScreen extends ConsumerWidget {
|
|||
Icon(
|
||||
Icons.cloud_off,
|
||||
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),
|
||||
Text(
|
||||
|
|
@ -132,7 +133,7 @@ class ServerListScreen extends ConsumerWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.7),
|
||||
.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,72 +20,56 @@ class AppearanceSettingsScreen extends ConsumerWidget {
|
|||
_buildSectionHeader(context, 'Theme'),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.light_mode),
|
||||
title: const Text('Light'),
|
||||
trailing: Radio<ThemeMode>(
|
||||
value: ThemeMode.light,
|
||||
groupValue: themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(value);
|
||||
}
|
||||
child: RadioGroup<ThemeMode>(
|
||||
groupValue: themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref.read(themeNotifierProvider.notifier).setThemeMode(value);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.light_mode),
|
||||
title: const Text('Light'),
|
||||
trailing: const Radio<ThemeMode>(
|
||||
value: ThemeMode.light,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.light);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.light);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dark_mode),
|
||||
title: const Text('Dark'),
|
||||
trailing: Radio<ThemeMode>(
|
||||
value: ThemeMode.dark,
|
||||
groupValue: themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(value);
|
||||
}
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.dark_mode),
|
||||
title: const Text('Dark'),
|
||||
trailing: const Radio<ThemeMode>(
|
||||
value: ThemeMode.dark,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.dark);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.dark);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.brightness_auto),
|
||||
title: const Text('System'),
|
||||
subtitle: const Text('Follow device theme'),
|
||||
trailing: Radio<ThemeMode>(
|
||||
value: ThemeMode.system,
|
||||
groupValue: themeMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(value);
|
||||
}
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.brightness_auto),
|
||||
title: const Text('System'),
|
||||
subtitle: const Text('Follow device theme'),
|
||||
trailing: const Radio<ThemeMode>(
|
||||
value: ThemeMode.system,
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.system);
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.setThemeMode(ThemeMode.system);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
|
|||
|
|
@ -20,34 +20,36 @@ class FilterQualitySettingsScreen extends ConsumerWidget {
|
|||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _filterQualityOptions.entries) ...[
|
||||
if (entry.key != _filterQualityOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
title: Text(entry.value.label),
|
||||
subtitle: Text(entry.value.description),
|
||||
trailing: Radio<FilterQuality>(
|
||||
value: entry.key,
|
||||
groupValue: filterQuality,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(filterQualityNotifierProvider.notifier)
|
||||
.setFilterQuality(value);
|
||||
}
|
||||
child: RadioGroup<FilterQuality>(
|
||||
groupValue: filterQuality,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(filterQualityNotifierProvider.notifier)
|
||||
.setFilterQuality(value);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _filterQualityOptions.entries) ...[
|
||||
if (entry.key != _filterQualityOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -20,34 +20,36 @@ class PrecachePagesSettingsScreen extends ConsumerWidget {
|
|||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _precachePagesOptions.entries) ...[
|
||||
if (entry.key != _precachePagesOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
title: Text(entry.value.label),
|
||||
subtitle: Text(entry.value.description),
|
||||
trailing: Radio<int>(
|
||||
value: entry.key,
|
||||
groupValue: precachePages,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(precachePagesNotifierProvider.notifier)
|
||||
.setPrecachePages(value);
|
||||
}
|
||||
child: RadioGroup<int>(
|
||||
groupValue: precachePages,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(precachePagesNotifierProvider.notifier)
|
||||
.setPrecachePages(value);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _precachePagesOptions.entries) ...[
|
||||
if (entry.key != _precachePagesOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.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/precache_pages_provider.dart';
|
||||
import 'package:worldhopper/providers/reading_mode_provider.dart';
|
||||
|
|
@ -38,6 +39,7 @@ class ReadersSettingsScreen extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final einkMode = ref.watch(einkModeNotifierProvider);
|
||||
final readingMode = ref.watch(readingModeNotifierProvider);
|
||||
final filterQuality = ref.watch(filterQualityNotifierProvider);
|
||||
final precachePages = ref.watch(precachePagesNotifierProvider);
|
||||
|
|
@ -50,6 +52,25 @@ class ReadersSettingsScreen extends ConsumerWidget {
|
|||
),
|
||||
body: ListView(
|
||||
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'),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
|
|
|
|||
|
|
@ -20,34 +20,36 @@ class ReadingModeSettingsScreen extends ConsumerWidget {
|
|||
const SizedBox(height: 8),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _readingModeOptions.entries) ...[
|
||||
if (entry.key != _readingModeOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
title: Text(entry.value.label),
|
||||
subtitle: Text(entry.value.description),
|
||||
trailing: Radio<ReadingMode>(
|
||||
value: entry.key,
|
||||
groupValue: readingMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(readingModeNotifierProvider.notifier)
|
||||
.setReadingMode(value);
|
||||
}
|
||||
child: RadioGroup<ReadingMode>(
|
||||
groupValue: readingMode,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
ref
|
||||
.read(readingModeNotifierProvider.notifier)
|
||||
.setReadingMode(value);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final entry in _readingModeOptions.entries) ...[
|
||||
if (entry.key != _readingModeOptions.keys.first)
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(entry.value.icon),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class EpubDownloadService {
|
|||
}
|
||||
|
||||
final url = acquisitionLink.href;
|
||||
if (url == null || url.isEmpty) {
|
||||
if (url.isEmpty) {
|
||||
throw Exception('Invalid acquisition link URL');
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ class EpubDownloadService {
|
|||
if (acquisitionLink == null) return null;
|
||||
|
||||
final url = acquisitionLink.href;
|
||||
if (url == null || url.isEmpty) return null;
|
||||
if (url.isEmpty) return null;
|
||||
|
||||
final fileInfo = await _cacheManager.getFileFromCache(url);
|
||||
if (fileInfo != null && await fileInfo.file.exists()) {
|
||||
|
|
@ -92,7 +92,7 @@ class EpubDownloadService {
|
|||
if (acquisitionLink == null) return;
|
||||
|
||||
final url = acquisitionLink.href;
|
||||
if (url == null || url.isEmpty) return;
|
||||
if (url.isEmpty) return;
|
||||
|
||||
await _cacheManager.removeFile(url);
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ class EpubDownloadService {
|
|||
if (acquisitionLink == null) return false;
|
||||
|
||||
final url = acquisitionLink.href;
|
||||
if (url == null || url.isEmpty) return false;
|
||||
if (url.isEmpty) return false;
|
||||
|
||||
final fileInfo = await _cacheManager.getFileFromCache(url);
|
||||
return fileInfo != null && await fileInfo.file.exists();
|
||||
|
|
@ -118,7 +118,7 @@ class EpubDownloadService {
|
|||
}
|
||||
|
||||
final url = acquisitionLink.href;
|
||||
if (url == null || url.isEmpty) {
|
||||
if (url.isEmpty) {
|
||||
throw Exception('Invalid acquisition link URL');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// Service for caching publication cover images locally
|
||||
|
|
@ -36,7 +37,7 @@ class ImageCacheService {
|
|||
|
||||
return file.path;
|
||||
} catch (e) {
|
||||
print('Error caching cover: $e');
|
||||
debugPrint('Error caching cover: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@ class ImageCacheService {
|
|||
|
||||
return file.path;
|
||||
} catch (e) {
|
||||
print('Error caching thumbnail: $e');
|
||||
debugPrint('Error caching thumbnail: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class NextInSeriesOverlay extends ConsumerStatefulWidget {
|
|||
final String? feedUrl;
|
||||
final OPDSEntry entry;
|
||||
final bool isVisible;
|
||||
final bool einkMode;
|
||||
final Future<void> Function()? onBeforeNavigate;
|
||||
|
||||
const NextInSeriesOverlay({
|
||||
|
|
@ -27,6 +28,7 @@ class NextInSeriesOverlay extends ConsumerStatefulWidget {
|
|||
this.feedUrl,
|
||||
required this.entry,
|
||||
required this.isVisible,
|
||||
this.einkMode = false,
|
||||
this.onBeforeNavigate,
|
||||
});
|
||||
|
||||
|
|
@ -124,82 +126,84 @@ class _NextInSeriesOverlayState extends ConsumerState<NextInSeriesOverlay> {
|
|||
required String title,
|
||||
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(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Dismissible(
|
||||
key: ValueKey(key),
|
||||
direction: DismissDirection.down,
|
||||
onDismissed: (_) {
|
||||
setState(() {
|
||||
_dismissed = true;
|
||||
});
|
||||
},
|
||||
child: 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.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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: widget.einkMode
|
||||
? barContent
|
||||
: Dismissible(
|
||||
key: ValueKey(key),
|
||||
direction: DismissDirection.down,
|
||||
onDismissed: (_) {
|
||||
setState(() {
|
||||
_dismissed = true;
|
||||
});
|
||||
},
|
||||
child: barContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class PublicationCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -132,7 +132,7 @@ class PublicationCard extends StatelessWidget {
|
|||
child: Icon(
|
||||
entry.isNavigation ? Icons.folder : Icons.book,
|
||||
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)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -183,7 +183,7 @@ class RecentlyReadCard extends ConsumerWidget {
|
|||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class ServerCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -123,7 +123,7 @@ class ServerCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
|
|
@ -132,7 +132,7 @@ class ServerCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -145,7 +145,7 @@ class ServerCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
|
|
@ -154,7 +154,7 @@ class ServerCard extends StatelessWidget {
|
|||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.6),
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import FlutterMacOS
|
|||
import Foundation
|
||||
|
||||
import connectivity_plus
|
||||
import device_info_plus
|
||||
import flutter_inappwebview_macos
|
||||
import package_info_plus
|
||||
import shared_preferences_foundation
|
||||
|
|
@ -15,6 +16,7 @@ import wakelock_plus
|
|||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
|
|
|||
40
pubspec.lock
40
pubspec.lock
|
|
@ -149,10 +149,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.4.1"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -265,6 +265,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -588,18 +604,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.17"
|
||||
version: "0.12.18"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.13.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1033,10 +1049,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
version: "0.7.8"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1197,6 +1213,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ dependencies:
|
|||
wakelock_plus: ^1.2.8
|
||||
package_info_plus: ^9.0.0
|
||||
url_launcher: ^6.3.2
|
||||
device_info_plus: ^11.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
Loading…
Reference in a new issue