OhMyDitzzy commited on
Commit
a2dfa54
·
1 Parent(s): a852a48
src/modules/comic/ComicReader.css CHANGED
@@ -4,7 +4,7 @@
4
  left: 0;
5
  right: 0;
6
  bottom: 0;
7
- background: #0a0a0a;
8
  color: #fff;
9
  overflow: hidden;
10
  display: flex;
@@ -19,6 +19,7 @@
19
  justify-content: center;
20
  height: 100vh;
21
  gap: 1.5rem;
 
22
  }
23
 
24
  .reader-error .error-icon {
@@ -27,12 +28,13 @@
27
 
28
  .reader-error h2 {
29
  margin: 0;
30
- font-size: 2rem;
31
  }
32
 
33
  .reader-error p {
34
  margin: 0;
35
  opacity: 0.7;
 
36
  }
37
 
38
  .reader-error button {
@@ -56,43 +58,49 @@
56
  display: flex;
57
  align-items: center;
58
  justify-content: space-between;
59
- padding: 1rem 1.5rem;
60
- background: rgba(0, 0, 0, 0.9);
61
  backdrop-filter: blur(10px);
62
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
63
  position: relative;
64
  z-index: 100;
65
  transition: transform 0.3s;
 
66
  }
67
 
68
  .comic-reader.hide-controls .reader-header {
69
  transform: translateY(-100%);
70
  }
71
 
72
- .back-btn {
 
73
  padding: 0.5rem 1rem;
74
  background: rgba(255, 255, 255, 0.1);
75
  border: 1px solid rgba(255, 255, 255, 0.2);
76
  border-radius: 8px;
77
  color: white;
78
- font-size: 0.95rem;
79
  cursor: pointer;
80
  transition: all 0.3s;
 
 
81
  }
82
 
83
- .back-btn:hover {
 
84
  background: rgba(255, 255, 255, 0.2);
85
  }
86
 
87
  .reader-title {
88
  flex: 1;
89
  text-align: center;
90
- padding: 0 2rem;
 
91
  }
92
 
93
  .reader-title h1 {
94
  margin: 0;
95
- font-size: 1.25rem;
96
  font-weight: 600;
97
  white-space: nowrap;
98
  overflow: hidden;
@@ -100,29 +108,107 @@
100
  }
101
 
102
  .page-indicator {
103
- font-size: 0.85rem;
104
  opacity: 0.7;
 
 
105
  }
106
 
107
- .reader-actions {
108
- display: flex;
109
- gap: 0.5rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  }
111
 
112
- .reader-actions button {
 
 
 
113
  width: 40px;
114
  height: 40px;
115
  background: rgba(255, 255, 255, 0.1);
116
- border: 1px solid rgba(255, 255, 255, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  border-radius: 8px;
118
  color: white;
119
- font-size: 1.2rem;
120
  cursor: pointer;
 
121
  transition: all 0.3s;
122
  }
123
 
124
- .reader-actions button:hover {
125
- background: rgba(255, 255, 255, 0.2);
 
 
 
 
 
 
 
 
 
 
126
  }
127
 
128
  .page-jump-modal {
@@ -135,21 +221,18 @@
135
  display: flex;
136
  align-items: center;
137
  justify-content: center;
138
- z-index: 200;
 
139
  animation: fadeIn 0.2s;
140
  }
141
 
142
- @keyframes fadeIn {
143
- from { opacity: 0; }
144
- to { opacity: 1; }
145
- }
146
-
147
  .page-jump-content {
148
  background: #1a1a1a;
149
  padding: 2rem;
150
  border-radius: 16px;
151
  border: 1px solid rgba(255, 255, 255, 0.1);
152
- min-width: 300px;
 
153
  }
154
 
155
  .page-jump-content h3 {
@@ -166,6 +249,7 @@
166
  color: white;
167
  font-size: 1rem;
168
  margin-bottom: 1rem;
 
169
  }
170
 
171
  .page-jump-content input:focus {
@@ -193,7 +277,7 @@
193
  color: white;
194
  }
195
 
196
- .page-jump-actions button:first-child:hover {
197
  background: #5568d3;
198
  }
199
 
@@ -202,150 +286,38 @@
202
  color: white;
203
  }
204
 
205
- .page-jump-actions button:last-child:hover {
206
  background: rgba(255, 255, 255, 0.2);
207
  }
208
 
209
- .view-mode-toggle {
210
- position: fixed;
211
- top: 50%;
212
- right: 1rem;
213
- transform: translateY(-50%);
214
- display: flex;
215
- flex-direction: column;
216
- gap: 0.5rem;
217
- z-index: 90;
218
- transition: opacity 0.3s;
219
- }
220
-
221
- .comic-reader.hide-controls .view-mode-toggle {
222
- opacity: 0;
223
- pointer-events: none;
224
- }
225
-
226
- .view-mode-toggle button {
227
- width: 48px;
228
- height: 48px;
229
- background: rgba(0, 0, 0, 0.7);
230
- backdrop-filter: blur(10px);
231
- border: 1px solid rgba(255, 255, 255, 0.2);
232
- border-radius: 12px;
233
- color: white;
234
- font-size: 1.5rem;
235
- cursor: pointer;
236
- transition: all 0.3s;
237
- }
238
-
239
- .view-mode-toggle button:hover,
240
- .view-mode-toggle button.active {
241
- background: #667eea;
242
- border-color: #667eea;
243
- transform: scale(1.1);
244
- }
245
-
246
  .reader-content {
247
  flex: 1;
248
- overflow: auto;
249
- position: relative;
250
  background: #000;
 
251
  }
252
 
253
  .scroll-container {
254
  display: flex;
255
  flex-direction: column;
256
  align-items: center;
257
- padding: 2rem 0;
258
- gap: 1rem;
259
  }
260
 
261
- .scroll-container img {
262
- max-width: 100%;
263
  height: auto;
264
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
265
- transition: all 0.3s;
266
- }
267
-
268
- .scroll-container img.current-page {
269
- box-shadow: 0 0 0 3px #667eea;
270
- }
271
-
272
- .single-container {
273
- display: flex;
274
- align-items: center;
275
- justify-content: center;
276
- height: 100%;
277
- position: relative;
278
- padding: 2rem;
279
- }
280
-
281
- .single-container img {
282
- max-width: 100%;
283
- max-height: 100%;
284
- object-fit: contain;
285
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
286
- }
287
-
288
- .double-container {
289
- display: flex;
290
- align-items: center;
291
- justify-content: center;
292
- height: 100%;
293
- position: relative;
294
- padding: 2rem;
295
- }
296
-
297
- .double-pages {
298
- display: flex;
299
- gap: 1rem;
300
- max-height: 100%;
301
- align-items: center;
302
- }
303
-
304
- .double-pages img {
305
- max-height: 100%;
306
- max-width: 45%;
307
- object-fit: contain;
308
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
309
- }
310
-
311
- .nav-btn {
312
- position: absolute;
313
- top: 50%;
314
- transform: translateY(-50%);
315
- width: 60px;
316
- height: 60px;
317
- background: rgba(0, 0, 0, 0.5);
318
- backdrop-filter: blur(10px);
319
- border: 1px solid rgba(255, 255, 255, 0.2);
320
- border-radius: 50%;
321
- color: white;
322
- font-size: 2rem;
323
- cursor: pointer;
324
- transition: all 0.3s;
325
- z-index: 10;
326
- }
327
-
328
- .nav-btn:hover:not(:disabled) {
329
- background: rgba(102, 126, 234, 0.8);
330
- transform: translateY(-50%) scale(1.1);
331
- }
332
-
333
- .nav-btn:disabled {
334
- opacity: 0.3;
335
- cursor: not-allowed;
336
- }
337
-
338
- .nav-btn.prev {
339
- left: 1rem;
340
- }
341
-
342
- .nav-btn.next {
343
- right: 1rem;
344
  }
345
 
346
  .reader-footer {
347
- padding: 1rem 1.5rem;
348
- background: rgba(0, 0, 0, 0.9);
349
  backdrop-filter: blur(10px);
350
  border-top: 1px solid rgba(255, 255, 255, 0.1);
351
  position: relative;
@@ -357,138 +329,191 @@
357
  transform: translateY(100%);
358
  }
359
 
360
- .chapter-navigation {
361
  display: flex;
362
  align-items: center;
363
- gap: 1rem;
 
364
  }
365
 
366
- .chapter-nav-btn {
367
- padding: 0.75rem 1.5rem;
368
  background: rgba(102, 126, 234, 0.2);
369
  border: 1px solid rgba(102, 126, 234, 0.4);
370
  border-radius: 8px;
371
  color: white;
372
- font-size: 0.9rem;
373
  font-weight: 500;
374
  cursor: pointer;
375
  transition: all 0.3s;
376
  white-space: nowrap;
 
377
  }
378
 
379
- .chapter-nav-btn:hover {
380
  background: rgba(102, 126, 234, 0.4);
381
- transform: translateY(-2px);
382
  }
383
 
384
- .page-slider {
 
 
 
 
 
385
  flex: 1;
386
- padding: 0 1rem;
 
 
 
387
  }
388
 
389
- .page-slider input[type="range"] {
 
 
 
 
 
 
390
  width: 100%;
391
- height: 6px;
392
  background: rgba(255, 255, 255, 0.1);
393
- border-radius: 3px;
394
  outline: none;
395
  -webkit-appearance: none;
 
396
  }
397
 
398
- .page-slider input[type="range"]::-webkit-slider-thumb {
399
  -webkit-appearance: none;
400
- width: 18px;
401
- height: 18px;
402
  background: #667eea;
403
  border-radius: 50%;
404
  cursor: pointer;
405
- transition: all 0.3s;
406
- }
407
-
408
- .page-slider input[type="range"]::-webkit-slider-thumb:hover {
409
- background: #5568d3;
410
- transform: scale(1.2);
411
  }
412
 
413
- .page-slider input[type="range"]::-moz-range-thumb {
414
- width: 18px;
415
- height: 18px;
416
  background: #667eea;
417
  border: none;
418
  border-radius: 50%;
419
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  transition: all 0.3s;
 
 
 
 
421
  }
422
 
423
- .page-slider input[type="range"]::-moz-range-thumb:hover {
424
- background: #5568d3;
425
- transform: scale(1.2);
426
  }
427
 
428
- .shortcuts-hint {
429
- position: fixed;
430
- bottom: 1rem;
431
- left: 50%;
432
- transform: translateX(-50%);
433
- padding: 0.5rem 1rem;
434
- background: rgba(0, 0, 0, 0.7);
435
- backdrop-filter: blur(10px);
436
- border: 1px solid rgba(255, 255, 255, 0.1);
437
- border-radius: 20px;
438
- font-size: 0.75rem;
439
- opacity: 0.6;
440
- pointer-events: none;
441
- z-index: 80;
442
- transition: opacity 0.3s;
443
  }
444
 
445
- .comic-reader.hide-controls .shortcuts-hint {
446
- opacity: 0;
447
  }
448
 
449
- @media (max-width: 768px) {
450
- .reader-header {
451
- padding: 0.75rem 1rem;
452
- }
 
 
 
 
453
 
 
 
 
 
 
454
  .reader-title h1 {
455
- font-size: 1rem;
456
  }
457
 
458
- .reader-title {
459
- padding: 0 1rem;
460
  }
461
 
462
- .view-mode-toggle {
463
- right: 0.5rem;
 
 
464
  }
465
 
466
- .view-mode-toggle button {
467
- width: 40px;
468
- height: 40px;
469
- font-size: 1.2rem;
470
  }
471
 
472
- .nav-btn {
473
- width: 50px;
474
- height: 50px;
475
- font-size: 1.5rem;
476
  }
477
 
 
478
  .chapter-nav-btn {
479
- padding: 0.5rem 1rem;
480
- font-size: 0.8rem;
481
  }
482
 
483
- .double-pages {
484
- flex-direction: column;
 
485
  }
486
 
487
- .double-pages img {
488
- max-width: 100%;
489
  }
490
 
491
- .shortcuts-hint {
492
- display: none;
493
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
494
  }
 
4
  left: 0;
5
  right: 0;
6
  bottom: 0;
7
+ background: #000;
8
  color: #fff;
9
  overflow: hidden;
10
  display: flex;
 
19
  justify-content: center;
20
  height: 100vh;
21
  gap: 1.5rem;
22
+ padding: 2rem;
23
  }
24
 
25
  .reader-error .error-icon {
 
28
 
29
  .reader-error h2 {
30
  margin: 0;
31
+ font-size: 1.5rem;
32
  }
33
 
34
  .reader-error p {
35
  margin: 0;
36
  opacity: 0.7;
37
+ text-align: center;
38
  }
39
 
40
  .reader-error button {
 
58
  display: flex;
59
  align-items: center;
60
  justify-content: space-between;
61
+ padding: 0.75rem 1rem;
62
+ background: rgba(0, 0, 0, 0.95);
63
  backdrop-filter: blur(10px);
64
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
65
  position: relative;
66
  z-index: 100;
67
  transition: transform 0.3s;
68
+ gap: 0.5rem;
69
  }
70
 
71
  .comic-reader.hide-controls .reader-header {
72
  transform: translateY(-100%);
73
  }
74
 
75
+ .back-btn,
76
+ .menu-btn {
77
  padding: 0.5rem 1rem;
78
  background: rgba(255, 255, 255, 0.1);
79
  border: 1px solid rgba(255, 255, 255, 0.2);
80
  border-radius: 8px;
81
  color: white;
82
+ font-size: 0.9rem;
83
  cursor: pointer;
84
  transition: all 0.3s;
85
+ white-space: nowrap;
86
+ flex-shrink: 0;
87
  }
88
 
89
+ .back-btn:active,
90
+ .menu-btn:active {
91
  background: rgba(255, 255, 255, 0.2);
92
  }
93
 
94
  .reader-title {
95
  flex: 1;
96
  text-align: center;
97
+ min-width: 0;
98
+ padding: 0 0.5rem;
99
  }
100
 
101
  .reader-title h1 {
102
  margin: 0;
103
+ font-size: 0.9rem;
104
  font-weight: 600;
105
  white-space: nowrap;
106
  overflow: hidden;
 
108
  }
109
 
110
  .page-indicator {
111
+ font-size: 0.75rem;
112
  opacity: 0.7;
113
+ display: block;
114
+ margin-top: 0.25rem;
115
  }
116
 
117
+ .hamburger-menu {
118
+ position: fixed;
119
+ top: 0;
120
+ left: 0;
121
+ right: 0;
122
+ bottom: 0;
123
+ background: rgba(0, 0, 0, 0.9);
124
+ z-index: 200;
125
+ animation: fadeIn 0.2s;
126
+ }
127
+
128
+ @keyframes fadeIn {
129
+ from { opacity: 0; }
130
+ to { opacity: 1; }
131
+ }
132
+
133
+ .menu-content {
134
+ position: absolute;
135
+ top: 0;
136
+ right: 0;
137
+ width: 280px;
138
+ max-width: 80vw;
139
+ height: 100%;
140
+ background: #1a1a1a;
141
+ padding: 1rem;
142
+ overflow-y: auto;
143
+ animation: slideInRight 0.3s;
144
+ }
145
+
146
+ @keyframes slideInRight {
147
+ from { transform: translateX(100%); }
148
+ to { transform: translateX(0); }
149
  }
150
 
151
+ .close-menu {
152
+ position: absolute;
153
+ top: 1rem;
154
+ right: 1rem;
155
  width: 40px;
156
  height: 40px;
157
  background: rgba(255, 255, 255, 0.1);
158
+ border: none;
159
+ border-radius: 50%;
160
+ color: white;
161
+ font-size: 1.5rem;
162
+ cursor: pointer;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ }
167
+
168
+ .menu-section {
169
+ margin: 3rem 0 2rem 0;
170
+ padding-bottom: 1rem;
171
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
172
+ }
173
+
174
+ .menu-section:first-child {
175
+ margin-top: 1rem;
176
+ }
177
+
178
+ .menu-section h3 {
179
+ margin: 0 0 1rem 0;
180
+ font-size: 0.85rem;
181
+ opacity: 0.6;
182
+ text-transform: uppercase;
183
+ letter-spacing: 1px;
184
+ }
185
+
186
+ .menu-section button {
187
+ display: block;
188
+ width: 100%;
189
+ padding: 0.75rem;
190
+ background: rgba(255, 255, 255, 0.05);
191
+ border: 1px solid rgba(255, 255, 255, 0.1);
192
  border-radius: 8px;
193
  color: white;
194
+ text-align: left;
195
  cursor: pointer;
196
+ margin-bottom: 0.5rem;
197
  transition: all 0.3s;
198
  }
199
 
200
+ .menu-section button:active {
201
+ background: rgba(255, 255, 255, 0.15);
202
+ }
203
+
204
+ .menu-info {
205
+ padding: 0.5rem;
206
+ }
207
+
208
+ .menu-info p {
209
+ margin: 0.5rem 0;
210
+ opacity: 0.7;
211
+ font-size: 0.9rem;
212
  }
213
 
214
  .page-jump-modal {
 
221
  display: flex;
222
  align-items: center;
223
  justify-content: center;
224
+ z-index: 300;
225
+ padding: 1rem;
226
  animation: fadeIn 0.2s;
227
  }
228
 
 
 
 
 
 
229
  .page-jump-content {
230
  background: #1a1a1a;
231
  padding: 2rem;
232
  border-radius: 16px;
233
  border: 1px solid rgba(255, 255, 255, 0.1);
234
+ width: 100%;
235
+ max-width: 300px;
236
  }
237
 
238
  .page-jump-content h3 {
 
249
  color: white;
250
  font-size: 1rem;
251
  margin-bottom: 1rem;
252
+ box-sizing: border-box;
253
  }
254
 
255
  .page-jump-content input:focus {
 
277
  color: white;
278
  }
279
 
280
+ .page-jump-actions button:first-child:active {
281
  background: #5568d3;
282
  }
283
 
 
286
  color: white;
287
  }
288
 
289
+ .page-jump-actions button:last-child:active {
290
  background: rgba(255, 255, 255, 0.2);
291
  }
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  .reader-content {
294
  flex: 1;
295
+ overflow-y: auto;
296
+ overflow-x: hidden;
297
  background: #000;
298
+ -webkit-overflow-scrolling: touch;
299
  }
300
 
301
  .scroll-container {
302
  display: flex;
303
  flex-direction: column;
304
  align-items: center;
305
+ width: 100%;
306
+ min-height: 100%;
307
  }
308
 
309
+ .comic-page {
310
+ width: 100%;
311
  height: auto;
312
+ display: block;
313
+ margin: 0;
314
+ padding: 0;
315
+ border: none;
316
+ vertical-align: top;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
  .reader-footer {
320
+ background: rgba(0, 0, 0, 0.95);
 
321
  backdrop-filter: blur(10px);
322
  border-top: 1px solid rgba(255, 255, 255, 0.1);
323
  position: relative;
 
329
  transform: translateY(100%);
330
  }
331
 
332
+ .page-controls {
333
  display: flex;
334
  align-items: center;
335
+ gap: 0.5rem;
336
+ padding: 0.75rem 1rem;
337
  }
338
 
339
+ .nav-control-btn {
340
+ padding: 0.5rem 1rem;
341
  background: rgba(102, 126, 234, 0.2);
342
  border: 1px solid rgba(102, 126, 234, 0.4);
343
  border-radius: 8px;
344
  color: white;
345
+ font-size: 0.85rem;
346
  font-weight: 500;
347
  cursor: pointer;
348
  transition: all 0.3s;
349
  white-space: nowrap;
350
+ flex-shrink: 0;
351
  }
352
 
353
+ .nav-control-btn:active:not(:disabled) {
354
  background: rgba(102, 126, 234, 0.4);
 
355
  }
356
 
357
+ .nav-control-btn:disabled {
358
+ opacity: 0.3;
359
+ cursor: not-allowed;
360
+ }
361
+
362
+ .page-info {
363
  flex: 1;
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 0.5rem;
367
+ min-width: 0;
368
  }
369
 
370
+ .page-info span {
371
+ text-align: center;
372
+ font-size: 0.85rem;
373
+ opacity: 0.8;
374
+ }
375
+
376
+ .page-slider {
377
  width: 100%;
378
+ height: 4px;
379
  background: rgba(255, 255, 255, 0.1);
380
+ border-radius: 2px;
381
  outline: none;
382
  -webkit-appearance: none;
383
+ appearance: none;
384
  }
385
 
386
+ .page-slider::-webkit-slider-thumb {
387
  -webkit-appearance: none;
388
+ width: 16px;
389
+ height: 16px;
390
  background: #667eea;
391
  border-radius: 50%;
392
  cursor: pointer;
 
 
 
 
 
 
393
  }
394
 
395
+ .page-slider::-moz-range-thumb {
396
+ width: 16px;
397
+ height: 16px;
398
  background: #667eea;
399
  border: none;
400
  border-radius: 50%;
401
  cursor: pointer;
402
+ }
403
+
404
+ .chapter-navigation {
405
+ display: flex;
406
+ gap: 0.5rem;
407
+ padding: 0 1rem 0.75rem 1rem;
408
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
409
+ padding-top: 0.75rem;
410
+ }
411
+
412
+ .chapter-spacer {
413
+ flex: 1;
414
+ }
415
+
416
+ .chapter-nav-btn {
417
+ padding: 0.75rem 1rem;
418
+ background: rgba(102, 126, 234, 0.15);
419
+ border: 1px solid rgba(102, 126, 234, 0.3);
420
+ border-radius: 8px;
421
+ color: white;
422
+ font-size: 0.85rem;
423
+ font-weight: 500;
424
+ cursor: pointer;
425
  transition: all 0.3s;
426
+ white-space: nowrap;
427
+ max-width: 45%;
428
+ overflow: hidden;
429
+ text-overflow: ellipsis;
430
  }
431
 
432
+ .chapter-nav-btn:active {
433
+ background: rgba(102, 126, 234, 0.3);
 
434
  }
435
 
436
+ .chapter-nav-btn.prev {
437
+ text-align: left;
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
+ .chapter-nav-btn.next {
441
+ text-align: right;
442
  }
443
 
444
+ .spinner {
445
+ width: 50px;
446
+ height: 50px;
447
+ border: 4px solid rgba(255, 255, 255, 0.1);
448
+ border-top-color: #667eea;
449
+ border-radius: 50%;
450
+ animation: spin 1s linear infinite;
451
+ }
452
 
453
+ @keyframes spin {
454
+ to { transform: rotate(360deg); }
455
+ }
456
+
457
+ @media (min-width: 768px) {
458
  .reader-title h1 {
459
+ font-size: 1.1rem;
460
  }
461
 
462
+ .page-indicator {
463
+ font-size: 0.85rem;
464
  }
465
 
466
+ .back-btn,
467
+ .menu-btn {
468
+ padding: 0.5rem 1.5rem;
469
+ font-size: 1rem;
470
  }
471
 
472
+ .scroll-container {
473
+ max-width: 1200px;
474
+ margin: 0 auto;
 
475
  }
476
 
477
+ .comic-page {
478
+ max-width: 100%;
 
 
479
  }
480
 
481
+ .nav-control-btn,
482
  .chapter-nav-btn {
483
+ font-size: 0.95rem;
484
+ padding: 0.75rem 1.5rem;
485
  }
486
 
487
+ .back-btn:hover,
488
+ .menu-btn:hover {
489
+ background: rgba(255, 255, 255, 0.2);
490
  }
491
 
492
+ .nav-control-btn:hover:not(:disabled) {
493
+ background: rgba(102, 126, 234, 0.4);
494
  }
495
 
496
+ .chapter-nav-btn:hover {
497
+ background: rgba(102, 126, 234, 0.3);
498
  }
499
+
500
+ .menu-section button:hover {
501
+ background: rgba(255, 255, 255, 0.15);
502
+ }
503
+ }
504
+
505
+ @media (min-width: 1200px) {
506
+ .comic-page {
507
+ max-width: 900px;
508
+ }
509
+ }
510
+
511
+ .reader-content::-webkit-scrollbar {
512
+ width: 0;
513
+ height: 0;
514
+ }
515
+
516
+ .reader-content {
517
+ scrollbar-width: none;
518
+ -ms-overflow-style: none;
519
  }
src/modules/comic/ComicReader.tsx CHANGED
@@ -31,60 +31,36 @@ export function ComicReader() {
31
  const [loading, setLoading] = useState(true);
32
  const [error, setError] = useState('');
33
  const [currentPage, setCurrentPage] = useState(1);
34
- const [viewMode, setViewMode] = useState<'single' | 'double' | 'scroll'>('scroll');
35
  const [showControls, setShowControls] = useState(true);
36
  const [showPageJump, setShowPageJump] = useState(false);
37
  const [jumpPage, setJumpPage] = useState('');
38
- const [_, setWs] = useState<WebSocket | null>(null);
39
 
40
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
41
  const controlsTimeout = useRef<number | null>(null);
 
42
 
43
- // Get data from location state (passed from ComicLanding)
44
  const chapterData = location.state?.chapterData;
45
- const password = location.state?.password ||
46
- JSON.parse(sessionStorage.getItem(`comic_${sessionId}`) || '{}').password;
47
  const originalSessionId = location.state?.sessionId;
48
 
49
  useEffect(() => {
50
- // If we have chapter data from state, use it directly
51
  if (chapterData) {
52
  setData(chapterData);
53
  setLoading(false);
54
  return;
55
  }
56
 
57
- // Otherwise, this might be a direct link or refresh
58
  if (!password || !originalSessionId) {
59
  setError('Unauthorized access - please open from comic page');
60
  setLoading(false);
61
  return;
62
  }
63
 
64
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
65
- const wsUrl = `${protocol}//${window.location.host}/ws`;
66
- const websocket = new WebSocket(wsUrl);
67
-
68
- websocket.onopen = () => {
69
- console.log('[Reader WS] Connected');
70
- // Since we don't have chapter slug, we can't request
71
- // User should go back to comic landing page
72
- setError('Please open chapter from comic page');
73
- setLoading(false);
74
- };
75
-
76
- websocket.onerror = () => {
77
- setError('WebSocket connection failed');
78
- setLoading(false);
79
- };
80
-
81
- setWs(websocket);
82
-
83
- return () => {
84
- websocket.close();
85
- };
86
  }, [sessionId, password, chapterData, originalSessionId]);
87
-
88
  useEffect(() => {
89
  const resetTimeout = () => {
90
  if (controlsTimeout.current) {
@@ -93,6 +69,7 @@ export function ComicReader() {
93
  setShowControls(true);
94
  controlsTimeout.current = window.setTimeout(() => {
95
  setShowControls(false);
 
96
  }, 3000);
97
  };
98
 
@@ -109,72 +86,75 @@ export function ComicReader() {
109
  }
110
  };
111
  }, []);
112
-
113
- // Keyboard shortcuts
114
  useEffect(() => {
115
- const handleKeyPress = (e: KeyboardEvent) => {
116
- if (e.key === 'ArrowRight') nextPage();
117
- if (e.key === 'ArrowLeft') prevPage();
118
- if (e.key === 'Home') setCurrentPage(1);
119
- if (e.key === 'End') setCurrentPage(data?.totalImages || 1);
120
- if (e.key === 'f') toggleFullscreen();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  };
122
 
123
- window.addEventListener('keydown', handleKeyPress);
124
- return () => window.removeEventListener('keydown', handleKeyPress);
125
- }, [currentPage, data]);
 
 
 
126
 
127
- // Scroll to current page in scroll mode
128
- useEffect(() => {
129
- if (viewMode === 'scroll' && imageRefs.current[currentPage]) {
130
- imageRefs.current[currentPage]?.scrollIntoView({
131
- behavior: 'smooth',
132
- block: 'center'
133
- });
 
 
134
  }
135
- }, [currentPage, viewMode]);
136
 
137
  const nextPage = () => {
138
  if (!data) return;
139
  if (currentPage < data.totalImages) {
140
- setCurrentPage(currentPage + 1);
141
- } else if (data.nextChapter) {
142
- handleChapterNavigation();
143
  }
144
  };
145
 
146
  const prevPage = () => {
147
  if (currentPage > 1) {
148
- setCurrentPage(currentPage - 1);
149
- } else if (data?.prevChapter) {
150
- handleChapterNavigation();
151
  }
152
  };
153
 
154
- const handleChapterNavigation = () => {
155
- // Go back to comic landing and let it handle chapter loading
156
- navigate(-1);
157
-
158
- // Alternatively, we could navigate to the same comic landing
159
- // and trigger chapter load from there
160
- // But going back is simpler and maintains the flow
161
- };
162
-
163
  const handlePageJump = () => {
164
  const page = parseInt(jumpPage);
165
  if (page >= 1 && page <= (data?.totalImages || 1)) {
166
- setCurrentPage(page);
167
  setShowPageJump(false);
168
  setJumpPage('');
169
  }
170
  };
171
 
172
- const toggleFullscreen = () => {
173
- if (!document.fullscreenElement) {
174
- document.documentElement.requestFullscreen();
175
- } else {
176
- document.exitFullscreen();
177
- }
178
  };
179
 
180
  if (loading) {
@@ -198,7 +178,7 @@ export function ComicReader() {
198
  }
199
 
200
  return (
201
- <div className={`comic-reader ${viewMode}-mode ${!showControls ? 'hide-controls' : ''}`}>
202
  <div className="reader-header">
203
  <button className="back-btn" onClick={() => navigate(-1)}>
204
  ← Back
@@ -207,19 +187,61 @@ export function ComicReader() {
207
  <div className="reader-title">
208
  <h1>{data.title}</h1>
209
  <span className="page-indicator">
210
- Page {currentPage} / {data.totalImages}
211
  </span>
212
  </div>
213
 
214
- <div className="reader-actions">
215
- <button onClick={() => setShowPageJump(!showPageJump)} title="Jump to page">
216
- 📍
217
- </button>
218
- <button onClick={toggleFullscreen} title="Fullscreen">
219
-
220
- </button>
221
- </div>
222
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  {showPageJump && (
224
  <div className="page-jump-modal" onClick={() => setShowPageJump(false)}>
225
  <div className="page-jump-content" onClick={(e) => e.stopPropagation()}>
@@ -241,127 +263,76 @@ export function ComicReader() {
241
  </div>
242
  </div>
243
  )}
244
- <div className="view-mode-toggle">
245
- <button
246
- className={viewMode === 'single' ? 'active' : ''}
247
- onClick={() => setViewMode('single')}
248
- title="Single page"
249
- >
250
- 📄
251
- </button>
252
- <button
253
- className={viewMode === 'double' ? 'active' : ''}
254
- onClick={() => setViewMode('double')}
255
- title="Double page"
256
- >
257
- 📑
258
- </button>
259
- <button
260
- className={viewMode === 'scroll' ? 'active' : ''}
261
- onClick={() => setViewMode('scroll')}
262
- title="Scroll"
263
- >
264
- 📜
265
- </button>
266
- </div>
267
- <div className="reader-content">
268
- {viewMode === 'scroll' ? (
269
- <div className="scroll-container">
270
- {data.images.map((img) => (
271
- <img
272
- key={img.index}
273
- ref={(el) => (imageRefs.current[img.index] = el)}
274
- src={img.imageUrl}
275
- alt={`Page ${img.index}`}
276
- className={currentPage === img.index ? 'current-page' : ''}
277
- loading="lazy"
278
- onError={(e) => {
279
- (e.target as HTMLImageElement).src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" text-anchor="middle">Failed to load</text></svg>';
280
- }}
281
- />
282
- ))}
283
- </div>
284
- ) : viewMode === 'single' ? (
285
- <div className="single-container">
286
- <button className="nav-btn prev" onClick={prevPage} disabled={currentPage === 1 && !data.prevChapter}>
287
-
288
- </button>
289
  <img
290
- src={data.images[currentPage - 1]?.imageUrl}
291
- alt={`Page ${currentPage}`}
 
 
 
 
292
  onError={(e) => {
293
- (e.target as HTMLImageElement).src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><text x="50%" y="50%" text-anchor="middle">Failed to load</text></svg>';
294
  }}
295
  />
296
- <button className="nav-btn next" onClick={nextPage} disabled={currentPage === data.totalImages && !data.nextChapter}>
297
-
298
- </button>
299
- </div>
300
- ) : (
301
- <div className="double-container">
302
- <button className="nav-btn prev" onClick={prevPage} disabled={currentPage === 1 && !data.prevChapter}>
303
-
304
- </button>
305
- <div className="double-pages">
306
- {currentPage % 2 === 0 && currentPage > 1 && (
307
- <img
308
- src={data.images[currentPage - 2]?.imageUrl}
309
- alt={`Page ${currentPage - 1}`}
310
- className="left-page"
311
- />
312
- )}
313
- <img
314
- src={data.images[currentPage - 1]?.imageUrl}
315
- alt={`Page ${currentPage}`}
316
- className={currentPage % 2 === 0 ? 'right-page' : 'left-page'}
317
- />
318
- {currentPage % 2 !== 0 && currentPage < data.totalImages && (
319
- <img
320
- src={data.images[currentPage]?.imageUrl}
321
- alt={`Page ${currentPage + 1}`}
322
- className="right-page"
323
- />
324
- )}
325
- </div>
326
- <button className="nav-btn next" onClick={nextPage} disabled={currentPage === data.totalImages && !data.nextChapter}>
327
-
328
- </button>
329
- </div>
330
- )}
331
  </div>
332
  <div className="reader-footer">
333
- <div className="chapter-navigation">
334
- {data.prevChapter && (
335
- <button
336
- className="chapter-nav-btn"
337
- onClick={() => handleChapterNavigation()}
338
- >
339
- {data.prevChapter.chapter}
340
- </button>
341
- )}
342
 
343
- <div className="page-slider">
 
344
  <input
345
  type="range"
346
  min="1"
347
  max={data.totalImages}
348
  value={currentPage}
349
- onChange={(e) => setCurrentPage(parseInt(e.target.value))}
 
350
  />
351
  </div>
352
 
353
- {data.nextChapter && (
354
- <button
355
- className="chapter-nav-btn"
356
- onClick={() => handleChapterNavigation()}
357
- >
358
- {data.nextChapter.chapter}
359
- </button>
360
- )}
361
  </div>
362
- </div>
363
- <div className="shortcuts-hint">
364
- Navigate | Home/End | F Fullscreen
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  </div>
366
  </div>
367
  );
 
31
  const [loading, setLoading] = useState(true);
32
  const [error, setError] = useState('');
33
  const [currentPage, setCurrentPage] = useState(1);
34
+ const [showMenu, setShowMenu] = useState(false);
35
  const [showControls, setShowControls] = useState(true);
36
  const [showPageJump, setShowPageJump] = useState(false);
37
  const [jumpPage, setJumpPage] = useState('');
 
38
 
39
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
40
  const controlsTimeout = useRef<number | null>(null);
41
+ const scrollContainerRef = useRef<HTMLDivElement | null>(null);
42
 
 
43
  const chapterData = location.state?.chapterData;
44
+ const password = location.state?.password;
 
45
  const originalSessionId = location.state?.sessionId;
46
 
47
  useEffect(() => {
 
48
  if (chapterData) {
49
  setData(chapterData);
50
  setLoading(false);
51
  return;
52
  }
53
 
 
54
  if (!password || !originalSessionId) {
55
  setError('Unauthorized access - please open from comic page');
56
  setLoading(false);
57
  return;
58
  }
59
 
60
+ setError('Please open chapter from comic page');
61
+ setLoading(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }, [sessionId, password, chapterData, originalSessionId]);
63
+
64
  useEffect(() => {
65
  const resetTimeout = () => {
66
  if (controlsTimeout.current) {
 
69
  setShowControls(true);
70
  controlsTimeout.current = window.setTimeout(() => {
71
  setShowControls(false);
72
+ setShowMenu(false);
73
  }, 3000);
74
  };
75
 
 
86
  }
87
  };
88
  }, []);
89
+
 
90
  useEffect(() => {
91
+ const handleScroll = () => {
92
+ if (!scrollContainerRef.current) return;
93
+
94
+ const container = scrollContainerRef.current;
95
+ const scrollTop = container.scrollTop;
96
+ const windowHeight = container.clientHeight;
97
+ let currentIdx = 1;
98
+
99
+ for (let i = 1; i <= (data?.totalImages || 0); i++) {
100
+ const img = imageRefs.current[i];
101
+ if (img) {
102
+ const rect = img.getBoundingClientRect();
103
+ const containerRect = container.getBoundingClientRect();
104
+ const imgCenter = rect.top + rect.height / 2 - containerRect.top;
105
+ if (imgCenter > 0 && imgCenter < windowHeight) {
106
+ currentIdx = i;
107
+ break;
108
+ }
109
+ }
110
+ }
111
+
112
+ setCurrentPage(currentIdx);
113
  };
114
 
115
+ const container = scrollContainerRef.current;
116
+ if (container) {
117
+ container.addEventListener('scroll', handleScroll);
118
+ return () => container.removeEventListener('scroll', handleScroll);
119
+ }
120
+ }, [data]);
121
 
122
+ const scrollToPage = (page: number) => {
123
+ const img = imageRefs.current[page];
124
+ if (img && scrollContainerRef.current) {
125
+ const container = scrollContainerRef.current;
126
+ const containerRect = container.getBoundingClientRect();
127
+ const imgRect = img.getBoundingClientRect();
128
+
129
+ const scrollTo = container.scrollTop + (imgRect.top - containerRect.top);
130
+ container.scrollTo({ top: scrollTo, behavior: 'smooth' });
131
  }
132
+ };
133
 
134
  const nextPage = () => {
135
  if (!data) return;
136
  if (currentPage < data.totalImages) {
137
+ scrollToPage(currentPage + 1);
 
 
138
  }
139
  };
140
 
141
  const prevPage = () => {
142
  if (currentPage > 1) {
143
+ scrollToPage(currentPage - 1);
 
 
144
  }
145
  };
146
 
 
 
 
 
 
 
 
 
 
147
  const handlePageJump = () => {
148
  const page = parseInt(jumpPage);
149
  if (page >= 1 && page <= (data?.totalImages || 1)) {
150
+ scrollToPage(page);
151
  setShowPageJump(false);
152
  setJumpPage('');
153
  }
154
  };
155
 
156
+ const handleChapterNavigation = (chapterSlug: string) => {
157
+ navigate(-1);
 
 
 
 
158
  };
159
 
160
  if (loading) {
 
178
  }
179
 
180
  return (
181
+ <div className={`comic-reader ${!showControls ? 'hide-controls' : ''}`}>
182
  <div className="reader-header">
183
  <button className="back-btn" onClick={() => navigate(-1)}>
184
  ← Back
 
187
  <div className="reader-title">
188
  <h1>{data.title}</h1>
189
  <span className="page-indicator">
190
+ {currentPage} / {data.totalImages}
191
  </span>
192
  </div>
193
 
194
+ <button
195
+ className="menu-btn"
196
+ onClick={() => setShowMenu(!showMenu)}
197
+ >
198
+
199
+ </button>
 
 
200
  </div>
201
+ {showMenu && (
202
+ <div className="hamburger-menu" onClick={() => setShowMenu(false)}>
203
+ <div className="menu-content" onClick={(e) => e.stopPropagation()}>
204
+ <button onClick={() => setShowMenu(false)} className="close-menu">✕</button>
205
+
206
+ <div className="menu-section">
207
+ <h3>Navigation</h3>
208
+ <button onClick={() => { scrollToPage(1); setShowMenu(false); }}>
209
+ ⏮️ First Page
210
+ </button>
211
+ <button onClick={() => { scrollToPage(data.totalImages); setShowMenu(false); }}>
212
+ ⏭️ Last Page
213
+ </button>
214
+ <button onClick={() => { setShowPageJump(true); setShowMenu(false); }}>
215
+ 📍 Jump to Page
216
+ </button>
217
+ </div>
218
+
219
+ {(data.prevChapter || data.nextChapter) && (
220
+ <div className="menu-section">
221
+ <h3>Chapters</h3>
222
+ {data.prevChapter && (
223
+ <button onClick={() => handleChapterNavigation(data.prevChapter!.slug)}>
224
+ ⬅️ {data.prevChapter.chapter}
225
+ </button>
226
+ )}
227
+ {data.nextChapter && (
228
+ <button onClick={() => handleChapterNavigation(data.nextChapter!.slug)}>
229
+ ➡️ {data.nextChapter.chapter}
230
+ </button>
231
+ )}
232
+ </div>
233
+ )}
234
+
235
+ <div className="menu-section">
236
+ <h3>Info</h3>
237
+ <div className="menu-info">
238
+ <p>Total Pages: {data.totalImages}</p>
239
+ <p>Current: Page {currentPage}</p>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+ )}
245
  {showPageJump && (
246
  <div className="page-jump-modal" onClick={() => setShowPageJump(false)}>
247
  <div className="page-jump-content" onClick={(e) => e.stopPropagation()}>
 
263
  </div>
264
  </div>
265
  )}
266
+ <div className="reader-content" ref={scrollContainerRef}>
267
+ <div className="scroll-container">
268
+ {data.images.map((img) => (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  <img
270
+ key={img.index}
271
+ ref={(el) => (imageRefs.current[img.index] = el)}
272
+ src={img.imageUrl}
273
+ alt={`Page ${img.index}`}
274
+ className="comic-page"
275
+ loading="lazy"
276
  onError={(e) => {
277
+ (e.target as HTMLImageElement).src = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="800" height="1200"><rect width="800" height="1200" fill="%23222"/><text x="50%" y="50%" text-anchor="middle" fill="%23666" font-size="20">Failed to load page ' + img.index + '</text></svg>';
278
  }}
279
  />
280
+ ))}
281
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
  <div className="reader-footer">
284
+ <div className="page-controls">
285
+ <button
286
+ className="nav-control-btn"
287
+ onClick={prevPage}
288
+ disabled={currentPage === 1}
289
+ >
290
+ Prev
291
+ </button>
 
292
 
293
+ <div className="page-info">
294
+ <span>{currentPage} / {data.totalImages}</span>
295
  <input
296
  type="range"
297
  min="1"
298
  max={data.totalImages}
299
  value={currentPage}
300
+ onChange={(e) => scrollToPage(parseInt(e.target.value))}
301
+ className="page-slider"
302
  />
303
  </div>
304
 
305
+ <button
306
+ className="nav-control-btn"
307
+ onClick={nextPage}
308
+ disabled={currentPage === data.totalImages}
309
+ >
310
+ Next
311
+ </button>
 
312
  </div>
313
+ {(data.prevChapter || data.nextChapter) && (
314
+ <div className="chapter-navigation">
315
+ {data.prevChapter && (
316
+ <button
317
+ className="chapter-nav-btn prev"
318
+ onClick={() => handleChapterNavigation(data.prevChapter!.slug)}
319
+ >
320
+ ← {data.prevChapter.chapter}
321
+ </button>
322
+ )}
323
+
324
+ <div className="chapter-spacer"></div>
325
+
326
+ {data.nextChapter && (
327
+ <button
328
+ className="chapter-nav-btn next"
329
+ onClick={() => handleChapterNavigation(data.nextChapter!.slug)}
330
+ >
331
+ {data.nextChapter.chapter} →
332
+ </button>
333
+ )}
334
+ </div>
335
+ )}
336
  </div>
337
  </div>
338
  );