uview-plus 3.8.86 → 3.8.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/changelog.md +150 -3
  2. package/components/u-action-sheet/u-action-sheet.vue +21 -2
  3. package/components/u-barcode/u-barcode.vue +0 -2
  4. package/components/u-button/u-button.vue +2 -5
  5. package/components/u-canvas/u-canvas.vue +368 -74
  6. package/components/u-cell/u-cell.vue +8 -0
  7. package/components/u-datetime-picker/u-datetime-picker.vue +53 -4
  8. package/components/u-dragsort/u-dragsort.vue +55 -31
  9. package/components/u-icon/u-icon.vue +12 -3
  10. package/components/u-icon/util.js +81 -10
  11. package/components/u-index-item/u-index-item.vue +1 -1
  12. package/components/u-index-list/u-index-list.vue +1 -1
  13. package/components/u-novel-reader/content-normalizer.js +80 -0
  14. package/components/u-novel-reader/layout-engine.js +225 -0
  15. package/components/u-novel-reader/measure-adapter.js +69 -0
  16. package/components/u-novel-reader/novelReader.js +35 -0
  17. package/components/u-novel-reader/persistence.js +144 -0
  18. package/components/u-novel-reader/props.js +100 -0
  19. package/components/u-novel-reader/reader-catalog.vue +234 -0
  20. package/components/u-novel-reader/reader-content.vue +226 -0
  21. package/components/u-novel-reader/reader-core.js +151 -0
  22. package/components/u-novel-reader/reader-settings.vue +325 -0
  23. package/components/u-novel-reader/reader-toolbar.vue +313 -0
  24. package/components/u-novel-reader/theme-vars.scss +49 -0
  25. package/components/u-novel-reader/u-novel-reader.vue +836 -0
  26. package/components/u-poster/u-poster.vue +236 -219
  27. package/components/u-qrcode/qrcode.js +56 -49
  28. package/components/u-qrcode/u-qrcode.vue +159 -88
  29. package/components/u-scroll-list/scrollWxs.wxs +1 -1
  30. package/components/u-slider/u-slider.vue +1 -1
  31. package/components/u-swipe-action-item/index.wxs +43 -25
  32. package/components/u-swipe-action-item/nvue.js +9 -0
  33. package/components/u-swipe-action-item/other.js +32 -21
  34. package/components/u-swipe-action-item/props.js +5 -0
  35. package/components/u-swipe-action-item/swipeActionItem.js +1 -0
  36. package/components/u-swipe-action-item/u-swipe-action-item.vue +28 -4
  37. package/components/u-switch/u-switch.vue +1 -1
  38. package/components/u-tabbar/u-tabbar.vue +2 -1
  39. package/components/u-tabbar-item/u-tabbar-item.vue +64 -21
  40. package/components/u-tabs/u-tabs.vue +11 -8
  41. package/components/u-tabs-pro/u-tabs-pro.vue +212 -0
  42. package/components/u-text/text.js +1 -1
  43. package/components/u-text/u-text.vue +18 -6
  44. package/components/u-upload/u-upload.vue +1 -1
  45. package/components/u-waterfall/u-waterfall.vue +118 -65
  46. package/libs/config/config.js +1 -1
  47. package/libs/config/props.js +1 -0
  48. package/libs/function/index.js +66 -0
  49. package/libs/mixin/mixin.js +4 -50
  50. package/libs/root/index.js +78 -5
  51. package/libs/root/page.js +7 -7
  52. package/libs/root/root.js +73 -3
  53. package/libs/util/app-nvue-webview-canvas.js +486 -0
  54. package/libs/util/gcanvas/bridge/bridge-weex.js +0 -2
  55. package/libs/util/gcanvas/context-2d/FillStyleLinearGradient.js +0 -0
  56. package/libs/util/gcanvas/context-2d/FillStylePattern.js +0 -0
  57. package/libs/util/gcanvas/context-2d/FillStyleRadialGradient.js +0 -0
  58. package/libs/util/gcanvas/context-2d/RenderingContext.js +0 -0
  59. package/libs/util/gcanvas/context-webgl/ActiveInfo.js +0 -0
  60. package/libs/util/gcanvas/context-webgl/Buffer.js +0 -0
  61. package/libs/util/gcanvas/context-webgl/Framebuffer.js +0 -0
  62. package/libs/util/gcanvas/context-webgl/GLenum.js +0 -0
  63. package/libs/util/gcanvas/context-webgl/GLmethod.js +0 -0
  64. package/libs/util/gcanvas/context-webgl/GLtype.js +0 -0
  65. package/libs/util/gcanvas/context-webgl/Program.js +0 -0
  66. package/libs/util/gcanvas/context-webgl/Renderbuffer.js +0 -0
  67. package/libs/util/gcanvas/context-webgl/RenderingContext.js +0 -0
  68. package/libs/util/gcanvas/context-webgl/Shader.js +0 -0
  69. package/libs/util/gcanvas/context-webgl/ShaderPrecisionFormat.js +0 -0
  70. package/libs/util/gcanvas/context-webgl/Texture.js +0 -0
  71. package/libs/util/gcanvas/context-webgl/UniformLocation.js +0 -0
  72. package/libs/util/gcanvas/context-webgl/classUtils.js +0 -0
  73. package/libs/util/gcanvas/env/canvas.js +0 -0
  74. package/libs/util/gcanvas/env/image.js +0 -0
  75. package/libs/util/gcanvas/env/tool.js +0 -0
  76. package/libs/util/gcanvas/index.js +3 -3
  77. package/package.json +1 -1
  78. package/types/comps/swipeActionItem.d.ts +13 -0
  79. package/types/comps/tabs.d.ts +2 -1
  80. package/types/comps/text.d.ts +1 -1
  81. package/types/func.d.ts +23 -0
  82. package/types/index.d.ts +1 -0
@@ -0,0 +1,836 @@
1
+ <template>
2
+ <view
3
+ class="up-novel-reader"
4
+ :class="`theme-${resolvedSettings.theme}`"
5
+ :style="readerStyle"
6
+ >
7
+ <view class="up-novel-reader__viewport">
8
+ <reader-content
9
+ :mode="resolvedMode"
10
+ :paragraphs="normalizedContent.paragraphs"
11
+ :pages="layout.pages"
12
+ :page-index="pageIndex"
13
+ :scroll-top="scrollTop"
14
+ :settings="resolvedSettings"
15
+ :theme-tokens="themeTokens"
16
+ :loading="loading"
17
+ :error="error"
18
+ :page-animation="effectiveAnimation"
19
+ @content-scroll="handleContentScroll"
20
+ @page-change="handlePageChange"
21
+ @tap-zone="handleTapZone"
22
+ @retry="handleRetry"
23
+ >
24
+ <template #loading>
25
+ <slot name="loading"></slot>
26
+ </template>
27
+ <template #error="{ error: slotError, retry }">
28
+ <slot name="error" :error="slotError" :retry="retry"></slot>
29
+ </template>
30
+ <template #empty>
31
+ <slot name="empty"></slot>
32
+ </template>
33
+ </reader-content>
34
+ </view>
35
+ <view v-if="controlsVisible" class="up-novel-reader__controls">
36
+ <view class="up-novel-reader__top-toolbar">
37
+ <u-status-bar
38
+ v-if="safeAreaInsetTop"
39
+ :bg-color="themeTokens.toolbar"
40
+ ></u-status-bar>
41
+ <reader-toolbar
42
+ position="top"
43
+ :title="currentChapter && currentChapter.title"
44
+ :show-back="showBack"
45
+ :back-icon="backIcon"
46
+ :is-bookmarked="isCurrentBookmarked"
47
+ :theme-tokens="themeTokens"
48
+ @back="handleBack"
49
+ @toggle-catalog="catalogVisible = true"
50
+ @toggle-bookmark="toggleBookmark"
51
+ @toggle-controls="hideControls"
52
+ >
53
+ <template #top>
54
+ <slot name="top"></slot>
55
+ </template>
56
+ <template #toolbar-extra>
57
+ <slot name="toolbar-extra"></slot>
58
+ </template>
59
+ </reader-toolbar>
60
+ </view>
61
+ <view class="up-novel-reader__bottom-toolbar">
62
+ <reader-toolbar
63
+ position="bottom"
64
+ :progress="currentProgress"
65
+ :page-count="layout.pageCount"
66
+ :current-chapter-index="currentChapterIndex"
67
+ :chapter-count="chapters.length"
68
+ :has-previous="hasPreviousChapter"
69
+ :has-next="hasNextChapter"
70
+ :theme-tokens="themeTokens"
71
+ @previous="requestChapter('previous')"
72
+ @next="requestChapter('next')"
73
+ @toggle-settings="settingsVisible = true"
74
+ @toggle-controls="hideControls"
75
+ >
76
+ <template #bottom>
77
+ <slot name="bottom"></slot>
78
+ </template>
79
+ </reader-toolbar>
80
+ <u-safe-bottom
81
+ v-if="safeAreaInsetBottom"
82
+ :style="{ backgroundColor: themeTokens.toolbar }"
83
+ ></u-safe-bottom>
84
+ </view>
85
+ </view>
86
+ <u-popup
87
+ v-model:show="catalogVisible"
88
+ mode="left"
89
+ round="0"
90
+ :duration="effectiveAnimation ? 300 : 0"
91
+ :overlay-duration="effectiveAnimation ? 350 : 0"
92
+ :safe-area-inset-bottom="safeAreaInsetBottom"
93
+ :custom-style="catalogPopupStyle"
94
+ >
95
+ <reader-catalog
96
+ :chapters="chapters"
97
+ :current-chapter="currentChapter"
98
+ :bookmarks="resolvedBookmarks"
99
+ :progress="currentProgress"
100
+ :theme-tokens="themeTokens"
101
+ @chapter-select="handleChapterSelect"
102
+ @bookmark-select="handleBookmarkSelect"
103
+ >
104
+ <template #default>
105
+ <slot name="catalog"></slot>
106
+ </template>
107
+ </reader-catalog>
108
+ </u-popup>
109
+ <u-popup
110
+ v-model:show="settingsVisible"
111
+ mode="bottom"
112
+ round="18"
113
+ :duration="effectiveAnimation ? 300 : 0"
114
+ :overlay-duration="effectiveAnimation ? 350 : 0"
115
+ :safe-area-inset-bottom="safeAreaInsetBottom"
116
+ :custom-style="settingsPopupStyle"
117
+ >
118
+ <reader-settings
119
+ :settings="resolvedSettings"
120
+ :theme-tokens="themeTokens"
121
+ @update-settings="handleSettingsUpdate"
122
+ @close="settingsVisible = false"
123
+ >
124
+ <template #default>
125
+ <slot name="settings"></slot>
126
+ </template>
127
+ </reader-settings>
128
+ </u-popup>
129
+ </view>
130
+ </template>
131
+
132
+ <script>
133
+ import { nextTick } from 'vue'
134
+ import { getWindowInfo } from '../../libs/function/index'
135
+ import { mixin } from '../../libs/mixin/mixin'
136
+ import { mpMixin } from '../../libs/mixin/mpMixin'
137
+ import { measureContainer, measureTextWidth } from './measure-adapter'
138
+ import { normalizeContent, normalizeProgress } from './content-normalizer'
139
+ import {
140
+ createBookmark,
141
+ mergeReaderSettings,
142
+ normalizeMode
143
+ } from './reader-core'
144
+ import { paginateParagraphs, resolveAnchor } from './layout-engine'
145
+ import {
146
+ createStorageKey,
147
+ readPersistedState,
148
+ writePersistedState
149
+ } from './persistence'
150
+ import props from './props'
151
+ import ReaderContent from './reader-content.vue'
152
+ import ReaderToolbar from './reader-toolbar.vue'
153
+ import ReaderCatalog from './reader-catalog.vue'
154
+ import ReaderSettings from './reader-settings.vue'
155
+
156
+ const THEME_TOKENS = {
157
+ day: {
158
+ background: '#f7f8fa',
159
+ text: '#303133',
160
+ muted: '#909399',
161
+ toolbar: '#ffffff',
162
+ border: 'rgba(48, 49, 51, 0.12)',
163
+ active: '#2979ff',
164
+ disabled: '#c8c9cc'
165
+ },
166
+ paper: {
167
+ background: '#f3ead7',
168
+ text: '#51483d',
169
+ muted: '#8f806d',
170
+ toolbar: '#f7efdf',
171
+ border: 'rgba(81, 72, 61, 0.16)',
172
+ active: '#9b7653',
173
+ disabled: '#c7b9a3'
174
+ },
175
+ green: {
176
+ background: '#e7f1e4',
177
+ text: '#3f5140',
178
+ muted: '#708371',
179
+ toolbar: '#eef6eb',
180
+ border: 'rgba(63, 81, 64, 0.16)',
181
+ active: '#4d8b55',
182
+ disabled: '#b6c7b4'
183
+ },
184
+ night: {
185
+ background: '#202124',
186
+ text: '#d6d7da',
187
+ muted: '#9ca0a8',
188
+ toolbar: '#292b30',
189
+ border: 'rgba(214, 215, 218, 0.16)',
190
+ active: '#7da7ff',
191
+ disabled: '#62656d'
192
+ },
193
+ dark: {
194
+ background: '#111214',
195
+ text: '#e5e7eb',
196
+ muted: '#9ca3af',
197
+ toolbar: '#1b1d21',
198
+ border: 'rgba(229, 231, 235, 0.16)',
199
+ active: '#8ab4ff',
200
+ disabled: '#5f6368'
201
+ }
202
+ }
203
+
204
+ export default {
205
+ name: 'up-novel-reader',
206
+ components: {
207
+ ReaderContent,
208
+ ReaderToolbar,
209
+ ReaderCatalog,
210
+ ReaderSettings
211
+ },
212
+ mixins: [mpMixin, mixin, props],
213
+ emits: [
214
+ 'chapter-request',
215
+ 'chapter-prefetch',
216
+ 'progress-change',
217
+ 'settings-change',
218
+ 'bookmark-change',
219
+ 'reading-time-change',
220
+ 'back',
221
+ 'mode-change',
222
+ 'toolbar-change',
223
+ 'layout-ready',
224
+ 'retry'
225
+ ],
226
+ data() {
227
+ return {
228
+ controlsVisible: false,
229
+ normalizedContent: {
230
+ paragraphs: [],
231
+ text: '',
232
+ length: 0
233
+ },
234
+ layout: {
235
+ pages: [],
236
+ pageCount: 0,
237
+ charOffsetToPage: []
238
+ },
239
+ pageIndex: 0,
240
+ scrollTop: 0,
241
+ viewport: {
242
+ width: 0,
243
+ height: 0
244
+ },
245
+ refreshToken: 0,
246
+ pendingChapter: null,
247
+ localSettings: null,
248
+ localProgress: null,
249
+ localBookmarks: [],
250
+ persistedState: null,
251
+ readerStorageKey: '',
252
+ persistTimer: null,
253
+ prefetchedTargets: {},
254
+ readingTime: 0,
255
+ readingActive: false,
256
+ readingLastActiveAt: 0,
257
+ scrollHeight: 0,
258
+ catalogVisible: false,
259
+ settingsVisible: false,
260
+ controlsHideTimer: null
261
+ }
262
+ },
263
+ created() {
264
+ this.initializeReaderState()
265
+ },
266
+ computed: {
267
+ resolvedSettings() {
268
+ return mergeReaderSettings(
269
+ this.defaultSettings,
270
+ this.localSettings,
271
+ this.settings
272
+ )
273
+ },
274
+ resolvedMode() {
275
+ return normalizeMode(this.mode)
276
+ },
277
+ themeTokens() {
278
+ const theme = THEME_TOKENS[this.resolvedSettings.theme]
279
+ ? this.resolvedSettings.theme
280
+ : 'day'
281
+ return {
282
+ ...THEME_TOKENS[theme],
283
+ theme
284
+ }
285
+ },
286
+ effectiveAnimation() {
287
+ return this.pageAnimation && this.resolvedSettings.animation
288
+ },
289
+ hasPreviousChapter() {
290
+ return this.chapters.some((chapter) => (
291
+ chapter &&
292
+ Number(chapter.index) < this.currentChapterIndex &&
293
+ !chapter.isLocked
294
+ ))
295
+ },
296
+ hasNextChapter() {
297
+ return this.chapters.some((chapter) => (
298
+ chapter &&
299
+ Number(chapter.index) > this.currentChapterIndex &&
300
+ !chapter.isLocked
301
+ ))
302
+ },
303
+ resolvedBookmarks() {
304
+ return Array.isArray(this.bookmarks)
305
+ ? this.bookmarks
306
+ : this.localBookmarks
307
+ },
308
+ isCurrentBookmarked() {
309
+ const chapterId = this.currentChapter && this.currentChapter.id
310
+ const charOffset = Number(this.currentProgress.charOffset) || 0
311
+ return this.resolvedBookmarks.some((bookmark) => (
312
+ bookmark &&
313
+ bookmark.chapterId === chapterId &&
314
+ Number(bookmark.charOffset) === charOffset
315
+ ))
316
+ },
317
+ readerStyle() {
318
+ return {
319
+ backgroundColor: this.themeTokens.background,
320
+ color: this.themeTokens.text,
321
+ '--up-novel-reader-background': this.themeTokens.background,
322
+ '--up-novel-reader-text': this.themeTokens.text,
323
+ '--up-novel-reader-muted': this.themeTokens.muted,
324
+ '--up-novel-reader-toolbar': this.themeTokens.toolbar,
325
+ '--up-novel-reader-active': this.themeTokens.active || '#2979ff',
326
+ '--up-novel-reader-disabled': this.themeTokens.disabled || '#c8c9cc',
327
+ '--up-novel-reader-border': this.themeTokens.border || 'rgba(48, 49, 51, 0.12)'
328
+ }
329
+ },
330
+ catalogPopupStyle() {
331
+ return {
332
+ width: '82vw',
333
+ maxWidth: '360px',
334
+ backgroundColor: this.themeTokens.background
335
+ }
336
+ },
337
+ settingsPopupStyle() {
338
+ return {
339
+ backgroundColor: this.themeTokens.background
340
+ }
341
+ },
342
+ currentChapterIndex() {
343
+ const index = Number(this.currentChapter && this.currentChapter.index)
344
+ if (Number.isFinite(index)) return index
345
+ return this.chapters.findIndex((item) => (
346
+ item && this.currentChapter && item.id === this.currentChapter.id
347
+ ))
348
+ },
349
+ currentProgress() {
350
+ const source = this.progress != null
351
+ ? this.progress
352
+ : (this.localProgress || this.initialProgress)
353
+ return normalizeProgress(source, this.currentChapter)
354
+ }
355
+ },
356
+ watch: {
357
+ currentChapter: {
358
+ handler(chapter) {
359
+ if (
360
+ this.pendingChapter &&
361
+ chapter &&
362
+ chapter.id === this.pendingChapter.targetId
363
+ ) {
364
+ this.pendingChapter = null
365
+ }
366
+ this.prefetchedTargets = {}
367
+ this.refreshContent()
368
+ },
369
+ deep: true,
370
+ immediate: true
371
+ },
372
+ mode() {
373
+ this.refreshLayout()
374
+ this.$emit('mode-change', this.resolvedMode)
375
+ },
376
+ settings: {
377
+ handler() {
378
+ this.refreshLayout()
379
+ this.queuePersist()
380
+ },
381
+ deep: true
382
+ },
383
+ progress: {
384
+ handler() {
385
+ this.syncProgress()
386
+ this.queuePersist()
387
+ },
388
+ deep: true
389
+ },
390
+ bookmarks: {
391
+ handler() {
392
+ this.queuePersist()
393
+ },
394
+ deep: true
395
+ },
396
+ loading(value) {
397
+ if (value) this.pauseReading()
398
+ },
399
+ error(value) {
400
+ if (value) this.pauseReading()
401
+ },
402
+ bookId() {
403
+ this.initializeReaderState()
404
+ },
405
+ storageKey() {
406
+ this.initializeReaderState()
407
+ }
408
+ },
409
+ mounted() {
410
+ this.refreshLayout()
411
+ if (typeof uni !== 'undefined' && typeof uni.onWindowResize === 'function') {
412
+ uni.onWindowResize(this.handleWindowResize)
413
+ }
414
+ },
415
+ beforeUnmount() {
416
+ this.pauseReading()
417
+ this.flushPersistence()
418
+ this.clearControlsHideTimer()
419
+ if (typeof uni !== 'undefined' && typeof uni.offWindowResize === 'function') {
420
+ uni.offWindowResize(this.handleWindowResize)
421
+ }
422
+ },
423
+ onHide() {
424
+ this.pauseReading()
425
+ this.flushPersistence()
426
+ },
427
+ methods: {
428
+ async refreshContent() {
429
+ this.normalizedContent = normalizeContent(
430
+ this.currentChapter && this.currentChapter.content
431
+ )
432
+ const progress = this.currentProgress
433
+ this.pageIndex = progress.pageIndex
434
+ this.scrollTop = progress.scrollTop
435
+ await nextTick()
436
+ this.refreshLayout()
437
+ },
438
+ async refreshLayout() {
439
+ const token = ++this.refreshToken
440
+ const rect = await measureContainer('.up-novel-reader__viewport', this)
441
+ if (token !== this.refreshToken) return
442
+ const windowInfo = getWindowInfo()
443
+ const width = Number(rect && rect.width) || Number(windowInfo.windowWidth) || 375
444
+ const height = Number(rect && rect.height) || Number(windowInfo.windowHeight) || 667
445
+ this.viewport = { width, height }
446
+ if (this.resolvedMode !== 'page') {
447
+ this.$emit('layout-ready', {
448
+ mode: this.resolvedMode,
449
+ width,
450
+ height,
451
+ pageCount: 0
452
+ })
453
+ return
454
+ }
455
+ const contentWidth = this.resolveContentWidth(width)
456
+ const layout = paginateParagraphs(this.normalizedContent.paragraphs, {
457
+ width: contentWidth,
458
+ height: Math.max(1, height - 64),
459
+ fontSize: this.resolvedSettings.fontSize,
460
+ lineHeight: this.resolvedSettings.lineHeight,
461
+ paragraphSpacing: this.resolvedSettings.paragraphSpacing,
462
+ fontFamily: this.resolvedSettings.fontFamily,
463
+ fontWeight: this.resolvedSettings.fontWeight,
464
+ measureText: (text) => measureTextWidth(text, this.resolvedSettings)
465
+ })
466
+ this.layout = layout
467
+ const anchor = resolveAnchor(layout.pages, this.currentProgress.charOffset)
468
+ this.pageIndex = anchor.pageIndex
469
+ this.$emit('layout-ready', {
470
+ mode: this.resolvedMode,
471
+ width,
472
+ height,
473
+ pageCount: layout.pageCount
474
+ })
475
+ },
476
+ resolveContentWidth(width) {
477
+ const value = this.resolvedSettings.contentWidth
478
+ if (typeof value === 'string' && value.endsWith('%')) {
479
+ return width * Math.min(100, Math.max(40, Number.parseFloat(value))) / 100
480
+ }
481
+ return Math.min(width, Math.max(1, Number.parseFloat(value) || width))
482
+ },
483
+ handleWindowResize() {
484
+ this.refreshLayout()
485
+ },
486
+ handleContentScroll(event) {
487
+ this.activateReading()
488
+ this.scrollTop = Math.max(0, Number(event && event.detail && event.detail.scrollTop) || 0)
489
+ this.scrollHeight = Math.max(
490
+ 0,
491
+ Number(event && event.detail && event.detail.scrollHeight) || this.scrollHeight
492
+ )
493
+ this.emitProgress({
494
+ scrollTop: this.scrollTop
495
+ })
496
+ },
497
+ handlePageChange(payload) {
498
+ this.activateReading()
499
+ this.pageIndex = Math.max(0, Number(payload && payload.pageIndex) || 0)
500
+ this.emitProgress({
501
+ pageIndex: this.pageIndex,
502
+ charOffset: this.layout.pages[this.pageIndex]
503
+ ? this.layout.pages[this.pageIndex].startOffset
504
+ : 0
505
+ })
506
+ },
507
+ handleTapZone(zone) {
508
+ this.activateReading()
509
+ if (zone === 'center') {
510
+ this.toggleControls('tap-center')
511
+ return
512
+ }
513
+ if (this.resolvedMode !== 'page') return
514
+ this.movePage(zone === 'left' ? -1 : 1)
515
+ },
516
+ toggleControls(reason = 'manual') {
517
+ this.controlsVisible = !this.controlsVisible
518
+ this.$emit('toolbar-change', {
519
+ visible: this.controlsVisible,
520
+ reason
521
+ })
522
+ if (this.controlsVisible) this.scheduleControlsHide()
523
+ else this.clearControlsHideTimer()
524
+ },
525
+ hideControls() {
526
+ if (!this.controlsVisible) return
527
+ this.controlsVisible = false
528
+ this.clearControlsHideTimer()
529
+ this.$emit('toolbar-change', {
530
+ visible: false,
531
+ reason: 'toolbar-action'
532
+ })
533
+ },
534
+ scheduleControlsHide() {
535
+ this.clearControlsHideTimer()
536
+ if (!this.controlsAutoHide) return
537
+ this.controlsHideTimer = setTimeout(() => {
538
+ this.hideControls()
539
+ }, Math.max(0, Number(this.controlsAutoHide)))
540
+ },
541
+ clearControlsHideTimer() {
542
+ if (this.controlsHideTimer) {
543
+ clearTimeout(this.controlsHideTimer)
544
+ this.controlsHideTimer = null
545
+ }
546
+ },
547
+ handleBack() {
548
+ this.$emit('back')
549
+ if (
550
+ this.autoBack &&
551
+ typeof uni !== 'undefined' &&
552
+ typeof uni.navigateBack === 'function'
553
+ ) {
554
+ uni.navigateBack()
555
+ }
556
+ },
557
+ handleChapterSelect(chapter) {
558
+ if (!chapter) return
559
+ this.catalogVisible = false
560
+ this.requestChapter('catalog', chapter.index)
561
+ },
562
+ handleBookmarkSelect(bookmark) {
563
+ if (!bookmark) return
564
+ this.catalogVisible = false
565
+ if (
566
+ this.currentChapter &&
567
+ bookmark.chapterId === this.currentChapter.id
568
+ ) {
569
+ this.pageIndex = Math.max(0, Number(bookmark.pageIndex) || 0)
570
+ this.scrollTop = Math.max(0, Number(bookmark.scrollTop) || 0)
571
+ this.emitProgress({
572
+ charOffset: Math.max(0, Number(bookmark.charOffset) || 0)
573
+ })
574
+ return
575
+ }
576
+ this.requestChapter('catalog', bookmark.chapterIndex)
577
+ },
578
+ handleSettingsUpdate(settings) {
579
+ this.localSettings = { ...settings }
580
+ this.settingsVisible = false
581
+ this.$emit('settings-change', {
582
+ mode: this.resolvedMode,
583
+ ...mergeReaderSettings(settings)
584
+ })
585
+ this.queuePersist()
586
+ this.refreshLayout()
587
+ },
588
+ toggleBookmark() {
589
+ const chapterId = this.currentChapter && this.currentChapter.id
590
+ if (!chapterId) return
591
+ const bookmark = createBookmark({
592
+ chapterId,
593
+ chapterIndex: this.currentChapterIndex,
594
+ charOffset: this.currentProgress.charOffset || 0,
595
+ pageIndex: this.pageIndex,
596
+ scrollTop: this.scrollTop,
597
+ excerpt: this.getBookmarkExcerpt()
598
+ })
599
+ const list = this.isCurrentBookmarked
600
+ ? this.resolvedBookmarks.filter((item) => item.id !== bookmark.id)
601
+ : [...this.resolvedBookmarks, bookmark]
602
+ if (!Array.isArray(this.bookmarks)) this.localBookmarks = list
603
+ this.$emit('bookmark-change', list)
604
+ this.queuePersist()
605
+ },
606
+ getBookmarkExcerpt() {
607
+ const text = this.normalizedContent.text || ''
608
+ const offset = Math.max(0, Number(this.currentProgress.charOffset) || 0)
609
+ return text.slice(Math.max(0, offset - 12), offset + 28).trim()
610
+ },
611
+ movePage(offset) {
612
+ const nextPageIndex = this.pageIndex + offset
613
+ if (nextPageIndex >= 0 && nextPageIndex < this.layout.pageCount) {
614
+ this.pageIndex = nextPageIndex
615
+ this.emitProgress({
616
+ pageIndex: nextPageIndex,
617
+ charOffset: this.layout.pages[nextPageIndex].startOffset
618
+ })
619
+ return
620
+ }
621
+ this.requestChapter(offset < 0 ? 'previous' : 'next')
622
+ },
623
+ requestChapter(direction, targetIndex) {
624
+ if (this.loading || this.pendingChapter) return false
625
+ const currentIndex = this.currentChapterIndex
626
+ const nextIndex = Number.isFinite(targetIndex)
627
+ ? targetIndex
628
+ : currentIndex + (direction === 'previous' ? -1 : 1)
629
+ const target = this.resolveChapter(nextIndex)
630
+ if (!target || target.isLocked) return false
631
+ const payload = {
632
+ targetIndex: nextIndex,
633
+ targetId: target.id,
634
+ direction,
635
+ requestId: `${Date.now()}-${this.refreshToken}`
636
+ }
637
+ this.pendingChapter = payload
638
+ this.flushPersistence()
639
+ this.$emit('chapter-request', payload)
640
+ return true
641
+ },
642
+ handleRetry() {
643
+ const retryPayload = this.pendingChapter
644
+ this.pendingChapter = null
645
+ this.$emit('retry', retryPayload)
646
+ },
647
+ syncProgress() {
648
+ if (!this.progress || !this.currentChapter) return
649
+ if (
650
+ this.progress.chapterId != null &&
651
+ this.progress.chapterId !== this.currentChapter.id
652
+ ) {
653
+ return
654
+ }
655
+ this.pageIndex = Math.max(0, Number(this.progress.pageIndex) || 0)
656
+ this.scrollTop = Math.max(0, Number(this.progress.scrollTop) || 0)
657
+ },
658
+ emitProgress(extra = {}) {
659
+ const page = this.layout.pages[this.pageIndex]
660
+ const pageCount = this.layout.pageCount
661
+ const scrollProgress = this.resolveScrollProgress()
662
+ const chapterProgress = pageCount
663
+ ? (this.pageIndex + 1) / pageCount
664
+ : scrollProgress
665
+ const charOffset = page
666
+ ? page.startOffset
667
+ : Math.round(this.normalizedContent.length * scrollProgress)
668
+ const progress = {
669
+ chapterId: this.currentChapter && this.currentChapter.id,
670
+ chapterIndex: this.currentChapterIndex,
671
+ pageIndex: this.pageIndex,
672
+ pageCount,
673
+ charOffset,
674
+ chapterProgress,
675
+ totalProgress: this.chapters.length
676
+ ? (this.currentChapterIndex + chapterProgress) / this.chapters.length
677
+ : 0,
678
+ scrollTop: this.scrollTop,
679
+ updatedAt: Date.now(),
680
+ ...extra
681
+ }
682
+ if (this.progress == null) this.localProgress = progress
683
+ this.$emit('progress-change', progress)
684
+ this.queuePersist()
685
+ this.emitPrefetchIfNeeded()
686
+ },
687
+ initializeReaderState() {
688
+ this.readerStorageKey = createStorageKey({
689
+ storageKey: this.storageKey,
690
+ bookId: this.bookId
691
+ })
692
+ this.persistedState = this.persist
693
+ ? readPersistedState(this.readerStorageKey)
694
+ : null
695
+ if (this.progress == null) {
696
+ this.localProgress = this.initialProgress || (
697
+ this.persistedState && this.persistedState.progress
698
+ ) || null
699
+ }
700
+ if (!Array.isArray(this.bookmarks)) {
701
+ this.localBookmarks = (
702
+ this.initialBookmarks && this.initialBookmarks.length
703
+ ? this.initialBookmarks
704
+ : (this.persistedState && this.persistedState.bookmarks) || []
705
+ ).slice()
706
+ }
707
+ if (this.settings == null) {
708
+ this.localSettings = (
709
+ this.persistedState && this.persistedState.settings
710
+ ) || null
711
+ }
712
+ this.readingTime = this.persistedState
713
+ ? this.persistedState.readingTime
714
+ : 0
715
+ },
716
+ resolveChapter(index) {
717
+ const numericIndex = Number(index)
718
+ return this.chapters.find((chapter) => (
719
+ chapter && Number(chapter.index) === numericIndex
720
+ )) || this.chapters[numericIndex]
721
+ },
722
+ resolveScrollProgress() {
723
+ if (this.scrollHeight <= this.viewport.height) return 0
724
+ const available = Math.max(1, this.scrollHeight - this.viewport.height)
725
+ return Math.min(1, Math.max(0, this.scrollTop / available))
726
+ },
727
+ emitPrefetchIfNeeded() {
728
+ if (
729
+ this.resolvedMode !== 'page' ||
730
+ !this.layout.pageCount ||
731
+ this.layout.pageCount - this.pageIndex > Math.max(0, this.preloadThreshold)
732
+ ) {
733
+ return
734
+ }
735
+ const target = this.chapters.find((chapter) => (
736
+ chapter &&
737
+ Number(chapter.index) > this.currentChapterIndex &&
738
+ !chapter.isLocked
739
+ ))
740
+ if (!target || this.prefetchedTargets[target.id]) return
741
+ this.prefetchedTargets[target.id] = true
742
+ this.$emit('chapter-prefetch', {
743
+ targetIndex: Number(target.index),
744
+ targetId: target.id,
745
+ direction: 'next'
746
+ })
747
+ },
748
+ queuePersist() {
749
+ if (!this.persist || !this.readerStorageKey) return
750
+ if (this.persistTimer) return
751
+ this.persistTimer = setTimeout(() => {
752
+ this.persistTimer = null
753
+ this.flushPersistence()
754
+ }, 180)
755
+ },
756
+ flushPersistence() {
757
+ if (this.persistTimer) {
758
+ clearTimeout(this.persistTimer)
759
+ this.persistTimer = null
760
+ }
761
+ if (!this.persist || !this.readerStorageKey) return false
762
+ return writePersistedState(this.readerStorageKey, {
763
+ progress: this.currentProgress,
764
+ settings: this.resolvedSettings,
765
+ bookmarks: this.resolvedBookmarks,
766
+ readingTime: this.readingTime
767
+ })
768
+ },
769
+ activateReading() {
770
+ if (this.loading || this.error || !this.currentChapter) return
771
+ if (!this.readingActive) {
772
+ this.readingActive = true
773
+ this.readingLastActiveAt = Date.now()
774
+ }
775
+ },
776
+ pauseReading() {
777
+ if (!this.readingActive) return
778
+ const now = Date.now()
779
+ const delta = Math.max(0, now - this.readingLastActiveAt)
780
+ this.readingTime += delta
781
+ this.readingActive = false
782
+ this.readingLastActiveAt = 0
783
+ this.$emit('reading-time-change', {
784
+ readingTime: this.readingTime,
785
+ delta,
786
+ updatedAt: now
787
+ })
788
+ this.queuePersist()
789
+ }
790
+ }
791
+ }
792
+ </script>
793
+
794
+ <style lang="scss" scoped>
795
+ @import "./theme-vars.scss";
796
+
797
+ .up-novel-reader {
798
+ position: relative;
799
+ width: 100%;
800
+ height: 100%;
801
+ overflow: hidden;
802
+ }
803
+
804
+ .up-novel-reader__controls,
805
+ .up-novel-reader__top-toolbar,
806
+ .up-novel-reader__bottom-toolbar {
807
+ position: absolute;
808
+ right: 0;
809
+ left: 0;
810
+ z-index: 10;
811
+ }
812
+
813
+ .up-novel-reader__controls {
814
+ top: 0;
815
+ bottom: 0;
816
+ pointer-events: none;
817
+ }
818
+
819
+ .up-novel-reader__top-toolbar,
820
+ .up-novel-reader__bottom-toolbar {
821
+ pointer-events: auto;
822
+ }
823
+
824
+ .up-novel-reader__top-toolbar {
825
+ top: 0;
826
+ }
827
+
828
+ .up-novel-reader__bottom-toolbar {
829
+ bottom: 0;
830
+ }
831
+
832
+ .up-novel-reader__viewport {
833
+ width: 100%;
834
+ height: 100%;
835
+ }
836
+ </style>