mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 05:49:03 +08:00
Add a bottom bar for single page viewer
This commit is contained in:
@@ -37,6 +37,11 @@
|
|||||||
"asc": "Ascending",
|
"asc": "Ascending",
|
||||||
"desc": "Descending",
|
"desc": "Descending",
|
||||||
"saveAs": "Save As",
|
"saveAs": "Save As",
|
||||||
|
"scrollDirection": "Scroll direction",
|
||||||
|
"scrollDirectionDefault": "Left to right",
|
||||||
|
"scrollDirectionRtl": "Right to left",
|
||||||
|
"scrollDirectionDown": "Top to bottom",
|
||||||
|
"scrollDirectionUp": "Bottom to top",
|
||||||
"posted": "Posted",
|
"posted": "Posted",
|
||||||
"visible": "Visible",
|
"visible": "Visible",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
"asc": "升序",
|
"asc": "升序",
|
||||||
"desc": "降序",
|
"desc": "降序",
|
||||||
"saveAs": "另存为",
|
"saveAs": "另存为",
|
||||||
|
"scrollDirection": "滚动方向",
|
||||||
|
"scrollDirectionDefault": "从左往右",
|
||||||
|
"scrollDirectionRtl": "从右往左",
|
||||||
|
"scrollDirectionDown": "从上往下",
|
||||||
|
"scrollDirectionUp": "从下往上",
|
||||||
"posted": "上传时间",
|
"posted": "上传时间",
|
||||||
"visible": "可见性",
|
"visible": "可见性",
|
||||||
"yes": "是",
|
"yes": "是",
|
||||||
|
|||||||
@@ -62,9 +62,158 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
Object? _error;
|
Object? _error;
|
||||||
bool _inited = false;
|
bool _inited = false;
|
||||||
bool _showMenu = false;
|
bool _showMenu = false;
|
||||||
|
int _scrollMethod = 0;
|
||||||
|
double? _sliderValue;
|
||||||
late PhotoViewController _photoViewController;
|
late PhotoViewController _photoViewController;
|
||||||
final LruMap<int, (Uint8List, String?, String)> _imgData =
|
final LruMap<int, (Uint8List, String?, String)> _imgData =
|
||||||
LruMap(maximumSize: 20);
|
LruMap(maximumSize: 20);
|
||||||
|
Axis get _scrollAxis => _scrollMethod >= 2 ? Axis.vertical : Axis.horizontal;
|
||||||
|
bool get _isReverseScroll => _scrollMethod == 1 || _scrollMethod == 3;
|
||||||
|
void _showPageSettings(BuildContext context) {
|
||||||
|
final i18n = AppLocalizations.of(context)!;
|
||||||
|
final options = [
|
||||||
|
MapEntry(0, i18n.scrollDirectionDefault),
|
||||||
|
MapEntry(1, i18n.scrollDirectionRtl),
|
||||||
|
MapEntry(2, i18n.scrollDirectionDown),
|
||||||
|
MapEntry(3, i18n.scrollDirectionUp)
|
||||||
|
];
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
var selected = _scrollMethod;
|
||||||
|
return StatefulBuilder(builder: (context, setSheetState) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(i18n.scrollDirection),
|
||||||
|
),
|
||||||
|
for (final entry in options)
|
||||||
|
RadioListTile<int>(
|
||||||
|
title: Text(entry.value),
|
||||||
|
value: entry.key,
|
||||||
|
groupValue: selected,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
setSheetState(() {
|
||||||
|
selected = value;
|
||||||
|
});
|
||||||
|
Navigator.of(sheetContext).pop();
|
||||||
|
final saved =
|
||||||
|
await prefs.setInt("single_viewer_scroll_method", value);
|
||||||
|
if (!saved) {
|
||||||
|
_log.warning(
|
||||||
|
"Failed to save single_viewer_scroll_method.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_scrollMethod = value;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomBar(BuildContext context) {
|
||||||
|
if (!_showMenu || _pages == null) return Container();
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final textStyle = theme.textTheme.bodyMedium;
|
||||||
|
final pagesCount = _pages!.length;
|
||||||
|
if (pagesCount == 0) return Container();
|
||||||
|
final double maxPage = (pagesCount - 1).toDouble();
|
||||||
|
final double currentValue =
|
||||||
|
(_sliderValue ?? _index.toDouble()).clamp(0.0, maxPage).toDouble();
|
||||||
|
final displayIndex = currentValue.round().clamp(0, pagesCount - 1);
|
||||||
|
|
||||||
|
Slider buildSlider() {
|
||||||
|
return Slider(
|
||||||
|
value: currentValue,
|
||||||
|
min: 0,
|
||||||
|
max: maxPage,
|
||||||
|
divisions: pagesCount - 1,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_sliderValue = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
final target = value.round().clamp(0, pagesCount - 1);
|
||||||
|
if (target != _index) {
|
||||||
|
_pageController.animateToPage(target,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_sliderValue = target.toDouble();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: SafeArea(
|
||||||
|
minimum: const EdgeInsets.all(16),
|
||||||
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
final isWide = constraints.maxWidth > 480;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: isWide
|
||||||
|
? Row(children: [
|
||||||
|
Text(
|
||||||
|
"${displayIndex + 1} / $pagesCount",
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
if (pagesCount > 1) ...[
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(child: buildSlider()),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
] else ...[
|
||||||
|
const Spacer(),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_showPageSettings(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings))
|
||||||
|
])
|
||||||
|
: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (pagesCount > 1) ...[
|
||||||
|
buildSlider(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
Row(children: [
|
||||||
|
Text(
|
||||||
|
"${displayIndex + 1} / $pagesCount",
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_showPageSettings(context);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings))
|
||||||
|
])
|
||||||
|
],
|
||||||
|
));
|
||||||
|
})));
|
||||||
|
}
|
||||||
void _updatePages() {
|
void _updatePages() {
|
||||||
if (_data == null) return;
|
if (_data == null) return;
|
||||||
final displayAd = prefs.getBool("displayAd") ?? false;
|
final displayAd = prefs.getBool("displayAd") ?? false;
|
||||||
@@ -76,6 +225,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
_pageController = PageController(initialPage: _index);
|
_pageController = PageController(initialPage: _index);
|
||||||
_inited = true;
|
_inited = true;
|
||||||
}
|
}
|
||||||
|
_sliderValue = _index.toDouble();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -84,6 +234,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
_updatePages();
|
_updatePages();
|
||||||
_files = widget.files;
|
_files = widget.files;
|
||||||
_back = "/gallery/${widget.gid}";
|
_back = "/gallery/${widget.gid}";
|
||||||
|
_scrollMethod = prefs.getInt("single_viewer_scroll_method") ?? 0;
|
||||||
_photoViewController = PhotoViewController();
|
_photoViewController = PhotoViewController();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
@@ -139,6 +290,8 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
scrollPhysics: const BouncingScrollPhysics(),
|
scrollPhysics: const BouncingScrollPhysics(),
|
||||||
pageController: _pageController,
|
pageController: _pageController,
|
||||||
itemCount: _pages!.length,
|
itemCount: _pages!.length,
|
||||||
|
scrollDirection: _scrollAxis,
|
||||||
|
reverse: _isReverseScroll,
|
||||||
builder: (BuildContext context, int index) {
|
builder: (BuildContext context, int index) {
|
||||||
final data = _pages![index];
|
final data = _pages![index];
|
||||||
final f = _files!.files[data.token]!.first;
|
final f = _files!.files[data.token]!.first;
|
||||||
@@ -160,7 +313,10 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onPageChanged: (index) {
|
onPageChanged: (index) {
|
||||||
_index = index;
|
setState(() {
|
||||||
|
_index = index;
|
||||||
|
_sliderValue = index.toDouble();
|
||||||
|
});
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
_onPageChanged(context);
|
_onPageChanged(context);
|
||||||
});
|
});
|
||||||
@@ -170,26 +326,55 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
|
|
||||||
Widget _buildWithKeyboardSupport(BuildContext context,
|
Widget _buildWithKeyboardSupport(BuildContext context,
|
||||||
{required Widget child}) {
|
{required Widget child}) {
|
||||||
|
void goPrevious() {
|
||||||
|
if (_index > 0) {
|
||||||
|
_pageController.previousPage(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void goNext() {
|
||||||
|
if (_index < _pages!.length - 1) {
|
||||||
|
_pageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final bindings = <KeyAction>[];
|
||||||
|
if (_scrollAxis == Axis.horizontal) {
|
||||||
|
if (_isReverseScroll) {
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowLeft, "next page", () => goNext()));
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowRight, "previous page", () => goPrevious()));
|
||||||
|
} else {
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowLeft, "previous page", () => goPrevious()));
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowRight, "next page", () => goNext()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_isReverseScroll) {
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowUp, "next page", () => goNext()));
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowDown, "previous page", () => goPrevious()));
|
||||||
|
} else {
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowUp, "previous page", () => goPrevious()));
|
||||||
|
bindings.add(KeyAction(
|
||||||
|
LogicalKeyboardKey.arrowDown, "next page", () => goNext()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings.add(KeyAction(LogicalKeyboardKey.backspace, "back", () {
|
||||||
|
context.canPop() ? context.pop() : context.go(_back);
|
||||||
|
}));
|
||||||
|
|
||||||
return KeyboardWidget(
|
return KeyboardWidget(
|
||||||
bindings: [
|
bindings: bindings,
|
||||||
KeyAction(LogicalKeyboardKey.arrowLeft, "previous page", () {
|
|
||||||
if (_index > 0) {
|
|
||||||
_pageController.previousPage(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeInOut);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
KeyAction(LogicalKeyboardKey.arrowRight, "next page", () {
|
|
||||||
if (_index < _pages!.length - 1) {
|
|
||||||
_pageController.nextPage(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeInOut);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
KeyAction(LogicalKeyboardKey.backspace, "back", () {
|
|
||||||
context.canPop() ? context.pop() : context.go(_back);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -254,11 +439,11 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
title: i18n.saveAs,
|
title: i18n.saveAs,
|
||||||
callback: () {
|
callback: () {
|
||||||
try {
|
try {
|
||||||
platformPath.saveFile(
|
platformPath.saveFile(
|
||||||
basenameWithoutExtension(_pages![_index].name),
|
basenameWithoutExtension(_pages![_index].name),
|
||||||
fmt.toMimeType(),
|
fmt.toMimeType(),
|
||||||
data,
|
data,
|
||||||
dir: isAndroid ? widget.gid!.toString() : "");
|
dir: isAndroid ? widget.gid.toString() : "");
|
||||||
} catch (err, stack) {
|
} catch (err, stack) {
|
||||||
_log.warning("Failed to save image: $err\n$stack");
|
_log.warning("Failed to save image: $err\n$stack");
|
||||||
}
|
}
|
||||||
@@ -360,6 +545,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
|
|||||||
body: Stack(children: [
|
body: Stack(children: [
|
||||||
_buildViewer(context),
|
_buildViewer(context),
|
||||||
_buildTopAppBar(context),
|
_buildTopAppBar(context),
|
||||||
|
_buildBottomBar(context),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user