@@ -62,9 +62,158 @@ class _SinglePageViewer extends State<SinglePageViewer>
6262 Object ? _error;
6363 bool _inited = false ;
6464 bool _showMenu = false ;
65+ int _scrollMethod = 0 ;
66+ double ? _sliderValue;
6567 late PhotoViewController _photoViewController;
6668 final LruMap <int , (Uint8List , String ?, String )> _imgData =
6769 LruMap (maximumSize: 20 );
70+ Axis get _scrollAxis => _scrollMethod >= 2 ? Axis .vertical : Axis .horizontal;
71+ bool get _isReverseScroll => _scrollMethod == 1 || _scrollMethod == 3 ;
72+ void _showPageSettings (BuildContext context) {
73+ final i18n = AppLocalizations .of (context)! ;
74+ final options = [
75+ MapEntry (0 , i18n.scrollDirectionDefault),
76+ MapEntry (1 , i18n.scrollDirectionRtl),
77+ MapEntry (2 , i18n.scrollDirectionDown),
78+ MapEntry (3 , i18n.scrollDirectionUp)
79+ ];
80+
81+ showModalBottomSheet (
82+ context: context,
83+ builder: (sheetContext) {
84+ var selected = _scrollMethod;
85+ return StatefulBuilder (builder: (context, setSheetState) {
86+ return SafeArea (
87+ child: Column (
88+ mainAxisSize: MainAxisSize .min,
89+ crossAxisAlignment: CrossAxisAlignment .stretch,
90+ children: [
91+ ListTile (
92+ dense: true ,
93+ title: Text (i18n.scrollDirection),
94+ ),
95+ for (final entry in options)
96+ RadioListTile <int >(
97+ title: Text (entry.value),
98+ value: entry.key,
99+ groupValue: selected,
100+ onChanged: (value) async {
101+ if (value == null ) return ;
102+ setSheetState (() {
103+ selected = value;
104+ });
105+ Navigator .of (sheetContext).pop ();
106+ final saved =
107+ await prefs.setInt ("single_viewer_scroll_method" , value);
108+ if (! saved) {
109+ _log.warning (
110+ "Failed to save single_viewer_scroll_method." );
111+ return ;
112+ }
113+ if (! mounted) return ;
114+ setState (() {
115+ _scrollMethod = value;
116+ });
117+ })
118+ ]));
119+ });
120+ });
121+ }
122+
123+ Widget _buildBottomBar (BuildContext context) {
124+ if (! _showMenu || _pages == null ) return Container ();
125+ final theme = Theme .of (context);
126+ final textStyle = theme.textTheme.bodyMedium;
127+ final pagesCount = _pages! .length;
128+ if (pagesCount == 0 ) return Container ();
129+ final double maxPage = (pagesCount - 1 ).toDouble ();
130+ final double currentValue =
131+ (_sliderValue ?? _index.toDouble ()).clamp (0.0 , maxPage).toDouble ();
132+ final displayIndex = currentValue.round ().clamp (0 , pagesCount - 1 );
133+
134+ Slider buildSlider () {
135+ return Slider (
136+ value: currentValue,
137+ min: 0 ,
138+ max: maxPage,
139+ divisions: pagesCount - 1 ,
140+ onChanged: (value) {
141+ setState (() {
142+ _sliderValue = value;
143+ });
144+ },
145+ onChangeEnd: (value) {
146+ final target = value.round ().clamp (0 , pagesCount - 1 );
147+ if (target != _index) {
148+ _pageController.animateToPage (target,
149+ duration: const Duration (milliseconds: 200 ),
150+ curve: Curves .easeInOut);
151+ }
152+ setState (() {
153+ _sliderValue = target.toDouble ();
154+ });
155+ },
156+ );
157+ }
158+
159+ return Positioned (
160+ left: 0 ,
161+ right: 0 ,
162+ bottom: 0 ,
163+ child: SafeArea (
164+ minimum: const EdgeInsets .all (16 ),
165+ child: LayoutBuilder (builder: (context, constraints) {
166+ final isWide = constraints.maxWidth > 480 ;
167+ return Container (
168+ padding: const EdgeInsets .symmetric (
169+ horizontal: 16 , vertical: 12 ),
170+ decoration: BoxDecoration (
171+ color: theme.colorScheme.surface.withOpacity (0.9 ),
172+ borderRadius: BorderRadius .circular (16 )),
173+ child: isWide
174+ ? Row (children: [
175+ Text (
176+ "${displayIndex + 1 } / $pagesCount " ,
177+ style: textStyle,
178+ ),
179+ if (pagesCount > 1 ) ...[
180+ const SizedBox (width: 16 ),
181+ Expanded (child: buildSlider ()),
182+ const SizedBox (width: 16 ),
183+ ] else ...[
184+ const Spacer (),
185+ const SizedBox (width: 16 ),
186+ ],
187+ IconButton (
188+ onPressed: () {
189+ _showPageSettings (context);
190+ },
191+ icon: const Icon (Icons .settings))
192+ ])
193+ : Column (
194+ mainAxisSize: MainAxisSize .min,
195+ crossAxisAlignment: CrossAxisAlignment .start,
196+ children: [
197+ if (pagesCount > 1 ) ...[
198+ buildSlider (),
199+ const SizedBox (height: 12 ),
200+ ],
201+ Row (children: [
202+ Text (
203+ "${displayIndex + 1 } / $pagesCount " ,
204+ style: textStyle,
205+ ),
206+ const Spacer (),
207+ IconButton (
208+ onPressed: () {
209+ _showPageSettings (context);
210+ },
211+ icon: const Icon (Icons .settings))
212+ ])
213+ ],
214+ ));
215+ })));
216+ }
68217 void _updatePages () {
69218 if (_data == null ) return ;
70219 final displayAd = prefs.getBool ("displayAd" ) ?? false ;
@@ -76,6 +225,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
76225 _pageController = PageController (initialPage: _index);
77226 _inited = true ;
78227 }
228+ _sliderValue = _index.toDouble ();
79229 }
80230
81231 @override
@@ -84,6 +234,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
84234 _updatePages ();
85235 _files = widget.files;
86236 _back = "/gallery/${widget .gid }" ;
237+ _scrollMethod = prefs.getInt ("single_viewer_scroll_method" ) ?? 0 ;
87238 _photoViewController = PhotoViewController ();
88239 super .initState ();
89240 }
@@ -139,6 +290,8 @@ class _SinglePageViewer extends State<SinglePageViewer>
139290 scrollPhysics: const BouncingScrollPhysics (),
140291 pageController: _pageController,
141292 itemCount: _pages! .length,
293+ scrollDirection: _scrollAxis,
294+ reverse: _isReverseScroll,
142295 builder: (BuildContext context, int index) {
143296 final data = _pages! [index];
144297 final f = _files! .files[data.token]! .first;
@@ -160,7 +313,10 @@ class _SinglePageViewer extends State<SinglePageViewer>
160313 );
161314 },
162315 onPageChanged: (index) {
163- _index = index;
316+ setState (() {
317+ _index = index;
318+ _sliderValue = index.toDouble ();
319+ });
164320 SchedulerBinding .instance.addPostFrameCallback ((_) {
165321 _onPageChanged (context);
166322 });
@@ -170,26 +326,55 @@ class _SinglePageViewer extends State<SinglePageViewer>
170326
171327 Widget _buildWithKeyboardSupport (BuildContext context,
172328 {required Widget child}) {
329+ void goPrevious () {
330+ if (_index > 0 ) {
331+ _pageController.previousPage (
332+ duration: const Duration (milliseconds: 200 ),
333+ curve: Curves .easeInOut);
334+ }
335+ }
336+
337+ void goNext () {
338+ if (_index < _pages! .length - 1 ) {
339+ _pageController.nextPage (
340+ duration: const Duration (milliseconds: 200 ),
341+ curve: Curves .easeInOut);
342+ }
343+ }
344+
345+ final bindings = < KeyAction > [];
346+ if (_scrollAxis == Axis .horizontal) {
347+ if (_isReverseScroll) {
348+ bindings.add (KeyAction (
349+ LogicalKeyboardKey .arrowLeft, "next page" , () => goNext ()));
350+ bindings.add (KeyAction (
351+ LogicalKeyboardKey .arrowRight, "previous page" , () => goPrevious ()));
352+ } else {
353+ bindings.add (KeyAction (
354+ LogicalKeyboardKey .arrowLeft, "previous page" , () => goPrevious ()));
355+ bindings.add (KeyAction (
356+ LogicalKeyboardKey .arrowRight, "next page" , () => goNext ()));
357+ }
358+ } else {
359+ if (_isReverseScroll) {
360+ bindings.add (KeyAction (
361+ LogicalKeyboardKey .arrowUp, "next page" , () => goNext ()));
362+ bindings.add (KeyAction (
363+ LogicalKeyboardKey .arrowDown, "previous page" , () => goPrevious ()));
364+ } else {
365+ bindings.add (KeyAction (
366+ LogicalKeyboardKey .arrowUp, "previous page" , () => goPrevious ()));
367+ bindings.add (KeyAction (
368+ LogicalKeyboardKey .arrowDown, "next page" , () => goNext ()));
369+ }
370+ }
371+
372+ bindings.add (KeyAction (LogicalKeyboardKey .backspace, "back" , () {
373+ context.canPop () ? context.pop () : context.go (_back);
374+ }));
375+
173376 return KeyboardWidget (
174- bindings: [
175- KeyAction (LogicalKeyboardKey .arrowLeft, "previous page" , () {
176- if (_index > 0 ) {
177- _pageController.previousPage (
178- duration: const Duration (milliseconds: 200 ),
179- curve: Curves .easeInOut);
180- }
181- }),
182- KeyAction (LogicalKeyboardKey .arrowRight, "next page" , () {
183- if (_index < _pages! .length - 1 ) {
184- _pageController.nextPage (
185- duration: const Duration (milliseconds: 200 ),
186- curve: Curves .easeInOut);
187- }
188- }),
189- KeyAction (LogicalKeyboardKey .backspace, "back" , () {
190- context.canPop () ? context.pop () : context.go (_back);
191- }),
192- ],
377+ bindings: bindings,
193378 child: child,
194379 );
195380 }
@@ -254,11 +439,11 @@ class _SinglePageViewer extends State<SinglePageViewer>
254439 title: i18n.saveAs,
255440 callback: () {
256441 try {
257- platformPath.saveFile (
258- basenameWithoutExtension (_pages! [_index].name),
259- fmt.toMimeType (),
260- data,
261- dir: isAndroid ? widget.gid! .toString () : "" );
442+ platformPath.saveFile (
443+ basenameWithoutExtension (_pages! [_index].name),
444+ fmt.toMimeType (),
445+ data,
446+ dir: isAndroid ? widget.gid.toString () : "" );
262447 } catch (err, stack) {
263448 _log.warning ("Failed to save image: $err \n $stack " );
264449 }
@@ -360,6 +545,7 @@ class _SinglePageViewer extends State<SinglePageViewer>
360545 body: Stack (children: [
361546 _buildViewer (context),
362547 _buildTopAppBar (context),
548+ _buildBottomBar (context),
363549 ]),
364550 );
365551 }
0 commit comments