1
+ import 'dart:io' ;
2
+
1
3
import 'package:cached_network_image/cached_network_image.dart' ;
2
4
import 'package:dartotsu/Widgets/ScrollConfig.dart' ;
5
+ import 'package:flutter/gestures.dart' ;
3
6
import 'package:flutter/material.dart' ;
7
+ import 'package:flutter/services.dart' ;
4
8
import 'package:get/get.dart' ;
9
+ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart' ;
5
10
import '../../../DataClass/Chapter.dart' ;
6
11
import '../../../DataClass/Media.dart' ;
7
12
import '../../../Api/Sources/Eval/dart/model/page.dart' ;
@@ -29,14 +34,37 @@ class MediaReader extends StatefulWidget {
29
34
}
30
35
31
36
class MediaReaderState extends State <MediaReader > {
32
- var showControls = true .obs;
33
- final focusNode = FocusNode ();
37
+ late final FocusNode focusNode = FocusNode ();
38
+ late final ItemScrollController itemScrollController = ItemScrollController ();
39
+ late final ItemPositionsListener itemPositionsListener = ItemPositionsListener .create ();
40
+ late final PageController pageController = PageController ();
41
+
42
+ final showControls = true .obs;
43
+ final currentPage = 1. obs;
44
+ final transformationController = TransformationController ();
45
+ double currentScale = 1.0 ;
34
46
35
47
@override
36
48
void initState () {
37
49
super .initState ();
38
50
focusNode.requestFocus ();
51
+ itemPositionsListener.itemPositions.addListener (_updateCurrentPage);
39
52
pageController.addListener (_onPageChanged);
53
+ if (Platform .isAndroid || Platform .isIOS) {
54
+ SystemChrome .setEnabledSystemUIMode (SystemUiMode .immersiveSticky);
55
+ }
56
+ }
57
+
58
+ @override
59
+ void dispose () {
60
+ focusNode.dispose ();
61
+ itemPositionsListener.itemPositions.removeListener (_updateCurrentPage);
62
+ pageController.removeListener (_onPageChanged);
63
+ if (Platform .isAndroid || Platform .isIOS) {
64
+ SystemChrome .setEnabledSystemUIMode (SystemUiMode .edgeToEdge);
65
+ }
66
+
67
+ super .dispose ();
40
68
}
41
69
42
70
@override
@@ -51,67 +79,46 @@ class MediaReaderState extends State<MediaReader> {
51
79
focusNode: focusNode,
52
80
child: GestureDetector (
53
81
onTap: () => showControls.value = ! showControls.value,
54
- child: Stack (
55
- alignment: Alignment .center,
56
- children: [
57
- InteractiveViewer (
58
- minScale: 0.5 ,
59
- maxScale: 4 ,
60
- child: _buildLTRMode (),
61
- ),
62
- _buildOverlay (),
63
- ],
82
+ onDoubleTap: _toggleZoom,
83
+ child: Listener (
84
+ onPointerSignal: (event) {
85
+ if (event is PointerScrollEvent && HardwareKeyboard .instance.isControlPressed) {
86
+ _zoomOnScroll (event.scrollDelta.dy);
87
+ }
88
+ },
89
+ child: Stack (
90
+ alignment: Alignment .center,
91
+ children: [
92
+ InteractiveViewer (
93
+ transformationController: transformationController,
94
+ minScale: 0.5 ,
95
+ maxScale: 4 ,
96
+ panEnabled: true ,
97
+ scaleEnabled: Platform .isAndroid || Platform .isIOS,
98
+ child: _buildUPDMode (),
99
+ ),
100
+ _buildOverlay (),
101
+ ],
102
+ ),
64
103
),
65
104
),
66
105
);
67
106
}
68
107
69
- var currentPage = 1 ;
70
-
71
- void _onPageChanged () {
72
- final page = (pageController.page? .round () ?? 0 ) + 1 ;
73
- if (page != currentPage) {
74
- setState (() => currentPage = page);
75
- }
76
- }
77
-
78
- ScrollController scrollController = ScrollController ();
79
-
80
108
Widget _buildUPDMode () {
81
109
return ScrollConfig (
82
110
context,
83
- child: ListView .builder (
84
- reverse: true ,
85
- controller: scrollController,
111
+ child: ScrollablePositionedList .builder (
86
112
itemCount: widget.pages.length,
113
+ itemScrollController: itemScrollController,
114
+ itemPositionsListener: itemPositionsListener,
87
115
itemBuilder: (context, index) {
88
- final page = widget.pages[index];
89
- return Center (
90
- child: ConstrainedBox (
91
- constraints:
92
- BoxConstraints (maxWidth: MediaQuery .of (context).size.width),
93
- child: CachedNetworkImage (
94
- imageUrl: page.url,
95
- fit: BoxFit .fitWidth,
96
- placeholder: (context, url) => SizedBox (
97
- height: MediaQuery .of (context).size.height / 2 ,
98
- child: Center (child: CircularProgressIndicator ()),
99
- ),
100
- errorWidget: (context, url, error) => Center (
101
- child: Text (
102
- 'Failed to load image: $error ' ,
103
- style: TextStyle (color: Colors .red),
104
- )),
105
- ),
106
- ),
107
- );
116
+ return _buildPageImage (widget.pages[index]);
108
117
},
109
118
),
110
119
);
111
120
}
112
121
113
- final PageController pageController = PageController ();
114
-
115
122
Widget _buildLTRMode () {
116
123
return ScrollConfig (
117
124
context,
@@ -121,30 +128,86 @@ class MediaReaderState extends State<MediaReader> {
121
128
reverse: true ,
122
129
itemCount: widget.pages.length,
123
130
itemBuilder: (context, index) {
124
- final page = widget.pages[index];
125
- return Center (
126
- child: ConstrainedBox (
127
- constraints:
128
- BoxConstraints (maxWidth: MediaQuery .of (context).size.width),
129
- child: CachedNetworkImage (
130
- imageUrl: page.url,
131
- fit: BoxFit .fitWidth,
132
- placeholder: (context, url) => SizedBox (
133
- height: MediaQuery .of (context).size.height / 2 ,
134
- child: Center (child: CircularProgressIndicator ()),
135
- ),
136
- errorWidget: (context, url, error) => Center (
137
- child: Text ('Failed to load image: $error ' ,
138
- style: TextStyle (color: Colors .red)),
131
+ return _buildPageImage (widget.pages[index]);
132
+ },
133
+ ),
134
+ );
135
+ }
136
+
137
+ Widget _buildPageImage (PageUrl page) {
138
+ return Center (
139
+ child: ConstrainedBox (
140
+ constraints: BoxConstraints (maxWidth: MediaQuery .of (context).size.width),
141
+ child: CachedNetworkImage (
142
+ imageUrl: page.url,
143
+ fit: BoxFit .fitWidth,
144
+ errorWidget: (context, url, error) => Center (
145
+ child: Text ('Failed to load image' , style: TextStyle (color: Colors .red)),
146
+ ),
147
+ progressIndicatorBuilder: (context, url, downloadProgress) {
148
+ return SizedBox (
149
+ height: MediaQuery .of (context).size.height / 2 ,
150
+ width: double .infinity,
151
+ child: Center (
152
+ child: CircularProgressIndicator (
153
+ value: downloadProgress.progress,
139
154
),
140
155
),
141
- ),
142
- );
143
- } ,
156
+ );
157
+ },
158
+ ) ,
144
159
),
145
160
);
146
161
}
147
162
163
+ void _onPageChanged () {
164
+ final page = (pageController.page? .round () ?? 0 ) + 1 ;
165
+ if (page != currentPage.value) {
166
+ currentPage.value = page;
167
+ }
168
+ }
169
+
170
+ void _updateCurrentPage () {
171
+ final positions = itemPositionsListener.itemPositions.value;
172
+
173
+ int mostVisiblePage = currentPage.value;
174
+ double maxVisibleFraction = 0 ;
175
+
176
+ for (final position in positions) {
177
+ final visibleFraction = _calculateVisibleFraction (position);
178
+
179
+ if (visibleFraction > maxVisibleFraction) {
180
+ maxVisibleFraction = visibleFraction;
181
+ mostVisiblePage = position.index + 1 ;
182
+ }
183
+ }
184
+
185
+ if (mostVisiblePage != currentPage.value && maxVisibleFraction >= 0.6 ) {
186
+ currentPage.value = mostVisiblePage;
187
+ }
188
+ }
189
+
190
+ double _calculateVisibleFraction (ItemPosition position) {
191
+ final viewportHeight = MediaQuery .of (context).size.height;
192
+ final itemTop = position.itemLeadingEdge * viewportHeight;
193
+ final itemBottom = position.itemTrailingEdge * viewportHeight;
194
+ final visibleTop = itemTop.clamp (0 , viewportHeight);
195
+ final visibleBottom = itemBottom.clamp (0 , viewportHeight);
196
+
197
+ return (visibleBottom - visibleTop) / (itemBottom - itemTop);
198
+ }
199
+
200
+ void _toggleZoom () {
201
+ currentScale = (currentScale < 2.0 ) ? 2.0 : 1.0 ;
202
+ transformationController.value = Matrix4 .identity ()..scale (currentScale);
203
+ }
204
+
205
+ void _zoomOnScroll (double scrollDelta) {
206
+ final zoomFactor = (scrollDelta < 0 ) ? 1.1 : 0.9 ;
207
+ currentScale = (currentScale * zoomFactor).clamp (1 , 4.0 );
208
+ transformationController.value = Matrix4 .identity ()..scale (currentScale);
209
+ }
210
+
148
211
Widget _buildOverlay () {
149
212
return Obx (() {
150
213
return Positioned .fill (
0 commit comments