generated from dellevin/template
670 lines
22 KiB
Dart
670 lines
22 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import '../../utils/epub/epub_theme.dart';
|
||
import '../../utils/epub/epub_webview_handler.dart';
|
||
import '../../utils/epub/epub_stream_service.dart';
|
||
import '../../utils/epub/epub_parser.dart';
|
||
import '../../utils/epub/reader_settings.dart';
|
||
import '../../utils/epub/reader_models.dart';
|
||
import '../../utils/epub/volume_control_service.dart';
|
||
import '../../utils/epub/reader_dao.dart';
|
||
import 'book_session.dart';
|
||
import 'reader_renderer.dart';
|
||
import 'control_panel.dart';
|
||
import 'toc_drawer.dart';
|
||
import 'image_viewer.dart';
|
||
import 'footnote_popup.dart';
|
||
|
||
part 'mixins/spine_navigation_mixin.dart';
|
||
part 'mixins/page_navigation_mixin.dart';
|
||
part 'mixins/progress_mixin.dart';
|
||
part 'mixins/theme_mixin.dart';
|
||
part 'mixins/link_handling_mixin.dart';
|
||
part 'mixins/image_viewer_mixin.dart';
|
||
part 'mixins/footnote_mixin.dart';
|
||
|
||
/// Reads EPUB directly from compressed file without extraction.
|
||
class ReaderScreen extends StatefulWidget {
|
||
final String bookId;
|
||
final String filePath;
|
||
final String title;
|
||
final String? coverPath;
|
||
final Map<String, dynamic>? bookData;
|
||
|
||
const ReaderScreen({
|
||
super.key,
|
||
required this.bookId,
|
||
required this.filePath,
|
||
required this.title,
|
||
this.coverPath,
|
||
this.bookData,
|
||
});
|
||
|
||
@override
|
||
State<ReaderScreen> createState() => _ReaderScreenState();
|
||
}
|
||
|
||
class _ReaderScreenState extends State<ReaderScreen>
|
||
with
|
||
WidgetsBindingObserver,
|
||
_SpineNavigationMixin,
|
||
_PageNavigationMixin,
|
||
_ProgressMixin,
|
||
_ThemeMixin,
|
||
_LinkHandlingMixin,
|
||
_ImageViewerMixin,
|
||
_FootnoteMixin {
|
||
@override
|
||
late final EpubWebViewHandler webViewHandler;
|
||
|
||
@override
|
||
late final BookSession bookSession;
|
||
|
||
@override
|
||
final ReaderRendererController rendererController =
|
||
ReaderRendererController();
|
||
|
||
// Core UI state
|
||
@override
|
||
bool isWebViewLoading = true;
|
||
|
||
@override
|
||
bool showControls = false;
|
||
|
||
// WebView visibility control for smoother transitions
|
||
Animation<double>? routeAnimation;
|
||
bool shouldShowWebView = false;
|
||
|
||
// Spine navigation state (used by _SpineNavigationMixin)
|
||
@override
|
||
int currentSpineItemIndex = 0;
|
||
|
||
// Pagination state (used by _PageNavigationMixin)
|
||
@override
|
||
int currentPageInChapter = 0;
|
||
@override
|
||
int totalPagesInChapter = 1;
|
||
|
||
// Progress state (used by _ProgressMixin)
|
||
@override
|
||
String displayProgress = '';
|
||
@override
|
||
Timer? progressDebouncer;
|
||
|
||
// Theme state (used by _ThemeMixin)
|
||
@override
|
||
ThemeData? currentTheme;
|
||
@override
|
||
bool updatingTheme = false;
|
||
@override
|
||
Timer? themeUpdateDebouncer;
|
||
@override
|
||
ReaderSettings readerSettings = const ReaderSettings();
|
||
|
||
// Image viewer state (used by _ImageViewerMixin)
|
||
@override
|
||
bool isImageViewerVisible = false;
|
||
@override
|
||
Uint8List? currentImageData;
|
||
@override
|
||
Rect? currentImageRect;
|
||
|
||
// Footnote state (used by _FootnoteMixin)
|
||
@override
|
||
OverlayEntry? footnoteOverlayEntry;
|
||
@override
|
||
final GlobalKey<FootnotePopupOverlayState> footnoteKey =
|
||
GlobalKey<FootnotePopupOverlayState>();
|
||
@override
|
||
bool isClosingFootnote = false;
|
||
|
||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||
|
||
StreamSubscription<String>? volumeSubscription;
|
||
bool tocDrawerOpen = false;
|
||
bool styleDrawerOpen = false;
|
||
AppLifecycleState? lastLifecycleState = AppLifecycleState.resumed;
|
||
|
||
// 书签
|
||
final List<Map<String, dynamic>> _bookmarks = [];
|
||
|
||
// Services
|
||
final EpubStreamService _streamService = EpubStreamService();
|
||
final ReaderDao _readerDao = ReaderDao();
|
||
final EpubParser _epubParser = EpubParser();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
webViewHandler = EpubWebViewHandler(streamService: _streamService);
|
||
|
||
// Create a placeholder BookSession; epubInfo will be replaced after parsing.
|
||
bookSession = BookSession(
|
||
fileHash: widget.bookId,
|
||
bookData: widget.bookData ?? {
|
||
'id': widget.bookId,
|
||
'file_path': widget.filePath,
|
||
'cover_path': widget.coverPath,
|
||
'last_read_cfi': '',
|
||
},
|
||
epubInfo: EpubBookInfo(
|
||
title: widget.title,
|
||
author: '',
|
||
authors: [],
|
||
opfRootPath: '',
|
||
epubVersion: '',
|
||
spine: [],
|
||
toc: [],
|
||
),
|
||
readerDao: _readerDao,
|
||
);
|
||
|
||
// Load settings first, then book
|
||
ReaderSettings.load().then((settings) {
|
||
readerSettings = settings;
|
||
if (mounted) {
|
||
setupVolumeControl();
|
||
_loadBook();
|
||
}
|
||
});
|
||
|
||
WidgetsBinding.instance.addObserver(this);
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
final route = ModalRoute.of(context);
|
||
if (route != null && route.animation != null) {
|
||
routeAnimation = route.animation!;
|
||
routeAnimation?.addStatusListener(handleRouteAnimationStatus);
|
||
} else {
|
||
shouldShowWebView = true;
|
||
}
|
||
});
|
||
hideBottomNavigationBar();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
routeAnimation?.removeStatusListener(handleRouteAnimationStatus);
|
||
routeAnimation = null;
|
||
themeUpdateDebouncer?.cancel();
|
||
progressDebouncer?.cancel();
|
||
removeFootnoteOverlay(animate: false);
|
||
restoreSystemUI();
|
||
volumeSubscription?.cancel();
|
||
VolumeControlService.disableInterception();
|
||
// 立即保存进度(同步写入,不被 dispose 打断)
|
||
bookSession.flushProgress(
|
||
currentChapterIndex: currentSpineItemIndex,
|
||
currentPageInChapter: currentPageInChapter,
|
||
totalPagesInChapter: totalPagesInChapter,
|
||
);
|
||
bookSession.dispose();
|
||
_streamService.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.paused ||
|
||
state == AppLifecycleState.inactive ||
|
||
state == AppLifecycleState.detached) {
|
||
saveProgress();
|
||
}
|
||
|
||
lastLifecycleState = state;
|
||
setupVolumeControl();
|
||
}
|
||
|
||
void setupVolumeControl() {
|
||
final resume = readerSettings.volumeKeyTurnsPage &&
|
||
!tocDrawerOpen &&
|
||
!styleDrawerOpen &&
|
||
lastLifecycleState == AppLifecycleState.resumed;
|
||
|
||
if (resume) {
|
||
VolumeControlService.enableInterception();
|
||
volumeSubscription ??= VolumeControlService.volumeKeyEvents.listen((
|
||
event,
|
||
) {
|
||
if (readerSettings.volumeKeyTurnsPage) {
|
||
if (footnoteOverlayEntry != null) {
|
||
removeFootnoteOverlay();
|
||
return;
|
||
}
|
||
if (event == 'up') {
|
||
rendererController.performPreviousPageTurn();
|
||
} else if (event == 'down') {
|
||
rendererController.performNextPageTurn();
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
VolumeControlService.disableInterception();
|
||
}
|
||
}
|
||
|
||
void hideBottomNavigationBar() {
|
||
SystemChrome.setEnabledSystemUIMode(
|
||
SystemUiMode.manual,
|
||
overlays: [SystemUiOverlay.top],
|
||
);
|
||
}
|
||
|
||
void restoreSystemUI() {
|
||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||
}
|
||
|
||
@override
|
||
void didChangeDependencies() {
|
||
super.didChangeDependencies();
|
||
|
||
if (currentTheme == null) {
|
||
currentTheme = Theme.of(context);
|
||
} else if (currentTheme?.colorScheme != Theme.of(context).colorScheme) {
|
||
currentTheme = Theme.of(context);
|
||
updateWebViewThemeWithDebounce();
|
||
}
|
||
}
|
||
|
||
void handleRouteAnimationStatus(AnimationStatus status) {
|
||
if (status == AnimationStatus.completed) {
|
||
setState(() {
|
||
shouldShowWebView = true;
|
||
});
|
||
routeAnimation?.removeStatusListener(handleRouteAnimationStatus);
|
||
routeAnimation = null;
|
||
}
|
||
}
|
||
|
||
/// Load book data from EPUB file and initialize session.
|
||
Future<void> _loadBook() async {
|
||
try {
|
||
// filePath is the original absolute path from import
|
||
final fullEpubPath = widget.filePath;
|
||
|
||
// Parse EPUB metadata using existing EpubParser
|
||
final epubInfo = await _epubParser.parseFromFile(
|
||
fullEpubPath,
|
||
fileName: widget.title,
|
||
);
|
||
|
||
if (epubInfo == null) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('EPUB 解析失败')),
|
||
);
|
||
Navigator.of(context).pop();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Open archive in stream service for WebView resource serving
|
||
await _streamService.openBook(fullEpubPath);
|
||
|
||
// Update session with parsed data
|
||
bookSession.updateEpubInfo(epubInfo);
|
||
bookSession.load();
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
currentSpineItemIndex = bookSession.initialChapterIndex;
|
||
});
|
||
updateProgressDebounced();
|
||
_loadBookmarks();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('加载书籍失败: $e')),
|
||
);
|
||
Navigator.of(context).pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─── 书签 ──────────────────────────────────────────────────────
|
||
|
||
Future<void> _loadBookmarks() async {
|
||
final list = await _readerDao.getBookmarksByBookId(widget.bookId);
|
||
if (mounted) setState(() => _bookmarks..clear()..addAll(list));
|
||
}
|
||
|
||
bool get _currentPageHasBookmark {
|
||
final cfi = '$currentSpineItemIndex:${(currentPageInChapter / (totalPagesInChapter > 0 ? totalPagesInChapter : 1)).toStringAsFixed(4)}';
|
||
return _bookmarks.any((bm) => (bm['cfi'] as String? ?? '') == cfi);
|
||
}
|
||
|
||
Future<void> _toggleBookmark() async {
|
||
// 检查当前页是否已有书签
|
||
final cfi = '$currentSpineItemIndex:${(currentPageInChapter / (totalPagesInChapter > 0 ? totalPagesInChapter : 1)).toStringAsFixed(4)}';
|
||
final existing = _bookmarks.where((bm) => (bm['cfi'] as String? ?? '') == cfi).firstOrNull;
|
||
|
||
if (existing != null) {
|
||
// 删除已有书签
|
||
await _readerDao.deleteBookmark(existing['id'] as int);
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('已移除书签'), duration: Duration(seconds: 1)),
|
||
);
|
||
}
|
||
} else {
|
||
// 添加书签
|
||
final chapterTitle = bookSession.spine.isNotEmpty &&
|
||
currentSpineItemIndex < bookSession.spine.length
|
||
? bookSession.spine[currentSpineItemIndex].href
|
||
: '';
|
||
// 尝试从 TOC 找更友好的标题
|
||
String title = chapterTitle;
|
||
for (final toc in bookSession.toc) {
|
||
if (toc.spineIndex == currentSpineItemIndex) {
|
||
title = toc.label;
|
||
break;
|
||
}
|
||
}
|
||
|
||
await _readerDao.insertBookmark({
|
||
'book_id': widget.bookId,
|
||
'content': title,
|
||
'cfi': cfi,
|
||
'chapter': currentSpineItemIndex.toString(),
|
||
'created_at': DateTime.now().toIso8601String(),
|
||
'updated_at': DateTime.now().toIso8601String(),
|
||
});
|
||
if (mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('已添加书签'), duration: Duration(seconds: 1)),
|
||
);
|
||
}
|
||
}
|
||
await _loadBookmarks();
|
||
}
|
||
|
||
void _jumpToBookmark(Map<String, dynamic> bookmark) {
|
||
final cfi = bookmark['cfi'] as String? ?? '';
|
||
if (cfi.isEmpty) return;
|
||
final parts = cfi.split(':');
|
||
if (parts.isEmpty) return;
|
||
final chapterIndex = int.tryParse(parts[0]) ?? 0;
|
||
final scrollRatio = parts.length >= 2 ? (double.tryParse(parts[1]) ?? 0.0) : 0.0;
|
||
|
||
if (chapterIndex >= 0 && chapterIndex < bookSession.spine.length) {
|
||
setState(() {
|
||
currentSpineItemIndex = chapterIndex;
|
||
});
|
||
loadCarousel(restoreScrollRatio: scrollRatio);
|
||
}
|
||
}
|
||
|
||
Future<void> _deleteBookmark(int id) async {
|
||
await _readerDao.deleteBookmark(id);
|
||
await _loadBookmarks();
|
||
}
|
||
|
||
void toggleControls() {
|
||
if (showControls) {
|
||
hideBottomNavigationBar();
|
||
} else {
|
||
restoreSystemUI();
|
||
}
|
||
setState(() {
|
||
showControls = !showControls;
|
||
});
|
||
}
|
||
|
||
void openDrawer() {
|
||
scaffoldKey.currentState?.openDrawer();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (!bookSession.isLoaded) {
|
||
return Scaffold(
|
||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||
body: const SizedBox.shrink(),
|
||
);
|
||
}
|
||
|
||
final epubTheme = getEpubTheme();
|
||
final isDark = epubTheme.isDark;
|
||
final colorScheme = epubTheme.colorScheme;
|
||
final themeData = Theme.of(context);
|
||
|
||
final overlayStyle = isDark
|
||
? SystemUiOverlayStyle.light.copyWith(
|
||
statusBarColor: Colors.transparent,
|
||
systemNavigationBarColor: colorScheme.surface,
|
||
systemNavigationBarIconBrightness: Brightness.light,
|
||
)
|
||
: SystemUiOverlayStyle.dark.copyWith(
|
||
statusBarColor: Colors.transparent,
|
||
systemNavigationBarColor: colorScheme.surface,
|
||
systemNavigationBarIconBrightness: Brightness.dark,
|
||
);
|
||
|
||
final activeItems = resolveActiveItems();
|
||
final activateTocTitle = activeItems.isNotEmpty
|
||
? activeItems.last.label
|
||
: widget.title;
|
||
|
||
return PopScope(
|
||
canPop: footnoteOverlayEntry == null,
|
||
onPopInvokedWithResult: (didPop, result) {
|
||
if (didPop) return;
|
||
if (footnoteOverlayEntry != null) {
|
||
removeFootnoteOverlay();
|
||
return;
|
||
}
|
||
// 和 lumina 一样:先保存,再 pop
|
||
saveProgress();
|
||
Navigator.of(context).pop();
|
||
},
|
||
child: AnnotatedRegion<SystemUiOverlayStyle>(
|
||
value: overlayStyle,
|
||
child: Stack(
|
||
children: [
|
||
Scaffold(
|
||
key: scaffoldKey,
|
||
backgroundColor: colorScheme.surfaceContainer,
|
||
drawer: TocDrawer(
|
||
bookTitle: widget.title,
|
||
coverPath: widget.coverPath,
|
||
totalChapters: bookSession.spine.length,
|
||
toc: bookSession.toc,
|
||
activeTocItems: activeItems,
|
||
currentSpineIndex: currentSpineItemIndex >= 0 && currentSpineItemIndex < bookSession.spine.length
|
||
? bookSession.spine[currentSpineItemIndex].index
|
||
: -1,
|
||
onTocItemSelected: navigateToTocItem,
|
||
onCoverTap: navigateToFirstTocItemFirstPage,
|
||
themeData: themeData,
|
||
bookmarks: _bookmarks,
|
||
onBookmarkTap: _jumpToBookmark,
|
||
onBookmarkDelete: _deleteBookmark,
|
||
),
|
||
onDrawerChanged: (isOpened) {
|
||
tocDrawerOpen = isOpened;
|
||
setupVolumeControl();
|
||
},
|
||
body: Container(
|
||
color: epubTheme.surfaceColor,
|
||
child: Stack(
|
||
children: [
|
||
ReaderRenderer(
|
||
controller: rendererController,
|
||
bookSession: bookSession,
|
||
webViewHandler: webViewHandler,
|
||
fileHash: widget.bookId,
|
||
showControls: showControls,
|
||
isLoading: isWebViewLoading || updatingTheme,
|
||
canPerformPageTurn: canPerformPageTurn,
|
||
onPerformPageTurn: handlePageTurn,
|
||
onToggleControls: toggleControls,
|
||
onInitialized: () async {
|
||
final ratio = bookSession.initialScrollPosition;
|
||
await loadCarousel(restoreScrollRatio: ratio);
|
||
},
|
||
onPageCountReady: (totalPages) async {
|
||
setState(() {
|
||
totalPagesInChapter = totalPages;
|
||
if (currentPageInChapter >= totalPagesInChapter) {
|
||
currentPageInChapter = totalPagesInChapter - 1;
|
||
}
|
||
});
|
||
updateProgressDebounced();
|
||
},
|
||
onPageChanged: (pageIndex) {
|
||
setState(() {
|
||
currentPageInChapter = pageIndex;
|
||
});
|
||
updateProgressDebounced();
|
||
saveProgress();
|
||
},
|
||
onScrollAnchors: handleScrollAnchors,
|
||
onImageLongPress: handleImageLongPress,
|
||
onFootnoteTap: handleFootnoteTap,
|
||
onLinkTap: handleLinkTap,
|
||
shouldHandleLinkTap: shouldHandleLinkTap,
|
||
shouldShowWebView: shouldShowWebView,
|
||
initializeTheme: epubTheme,
|
||
statusBarLeftContent: activateTocTitle,
|
||
statusBarRightContent: displayProgress,
|
||
pageAnimation: readerSettings.pageAnimation,
|
||
),
|
||
|
||
ControlPanel(
|
||
showControls: showControls,
|
||
title: bookSession.spine.isEmpty
|
||
? widget.title
|
||
: activateTocTitle,
|
||
currentSpineItemIndex: currentSpineItemIndex,
|
||
totalSpineItems: bookSession.spine.length,
|
||
currentPageInChapter: currentPageInChapter,
|
||
totalPagesInChapter: totalPagesInChapter,
|
||
direction: bookSession.direction,
|
||
fontSize: readerSettings.zoom * 18.0,
|
||
zoom: readerSettings.zoom,
|
||
marginTop: readerSettings.marginTop,
|
||
marginBottom: readerSettings.marginBottom,
|
||
marginLeft: readerSettings.marginLeft,
|
||
marginRight: readerSettings.marginRight,
|
||
onBack: () {
|
||
saveProgress();
|
||
Navigator.of(context).pop();
|
||
},
|
||
onOpenDrawer: openDrawer,
|
||
onPreviousPage: () =>
|
||
rendererController.performPreviousPageTurn(),
|
||
onFirstPage: () => goToPage(0),
|
||
onNextPage: () =>
|
||
rendererController.performNextPageTurn(),
|
||
onLastPage: () => goToPage(totalPagesInChapter - 1),
|
||
onPreviousChapter: previousSpineItemFirstPage,
|
||
onNextChapter: nextSpineItem,
|
||
onToggleStyleDrawer: () {
|
||
// Style sheet is opened internally by ControlPanel
|
||
},
|
||
onZoomChanged: (value) {
|
||
setState(() {
|
||
readerSettings = readerSettings.copyWith(zoom: value);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onFontSizeChanged: (value) {
|
||
// 将字号值映射为 zoom(12px→0.7, 18px→1.0, 32px→1.8)
|
||
final zoom = value / 18.0;
|
||
setState(() {
|
||
readerSettings = readerSettings.copyWith(zoom: zoom);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onMarginTopChanged: (value) {
|
||
setState(() {
|
||
readerSettings =
|
||
readerSettings.copyWith(marginTop: value);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onMarginBottomChanged: (value) {
|
||
setState(() {
|
||
readerSettings =
|
||
readerSettings.copyWith(marginBottom: value);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onMarginLeftChanged: (value) {
|
||
setState(() {
|
||
readerSettings =
|
||
readerSettings.copyWith(marginLeft: value);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onMarginRightChanged: (value) {
|
||
setState(() {
|
||
readerSettings =
|
||
readerSettings.copyWith(marginRight: value);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
themeIndex: readerSettings.themeIndex,
|
||
customBgColor: readerSettings.customBgColor,
|
||
customTextColor: readerSettings.customTextColor,
|
||
onThemeIndexChanged: (index) {
|
||
setState(() {
|
||
readerSettings = readerSettings.copyWith(themeIndex: index);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
onCustomColorChanged: (bgColor, textColor) {
|
||
setState(() {
|
||
readerSettings = readerSettings.copyWith(
|
||
customBgColor: bgColor,
|
||
customTextColor: textColor,
|
||
);
|
||
});
|
||
readerSettings.save();
|
||
updateWebViewTheme();
|
||
},
|
||
currentPageHasBookmark: _currentPageHasBookmark,
|
||
onBookmarkToggle: _toggleBookmark,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
Positioned.fill(
|
||
child: IgnorePointer(
|
||
ignoring: !isImageViewerVisible,
|
||
child: AnimatedOpacity(
|
||
duration: const Duration(milliseconds: 250),
|
||
curve: Curves.easeOut,
|
||
opacity: isImageViewerVisible ? 1.0 : 0.0,
|
||
child: (currentImageData != null && currentImageRect != null)
|
||
? ImageViewer(
|
||
imageData: currentImageData!,
|
||
onClose: closeImageViewer,
|
||
sourceRect: currentImageRect!,
|
||
colorScheme: colorScheme,
|
||
)
|
||
: const SizedBox.shrink(),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|