feat: 保存阅读进度书签
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
v-if="showProgress"
|
v-if="showProgress"
|
||||||
class="reading-progress"
|
class="reading-progress"
|
||||||
:class="{ 'is-dragging': isDragging }"
|
:class="{ 'is-dragging': isDragging }"
|
||||||
:title="isDragging ? '拖动调整位置' : '阅读进度 ' + progress + '%'"
|
:title="progressTitle"
|
||||||
@mousedown="startDrag"
|
@mousedown="startDrag"
|
||||||
@touchstart="startDrag"
|
@touchstart="startDrag"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<div v-else key="percent" class="progress-text">{{ progress }}%</div>
|
<div v-else key="percent" class="progress-text">{{ progress }}%</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<div v-if="!isDragging && bookmarkLabel" class="bookmark-label">
|
||||||
|
{{ bookmarkLabel }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 拖拽时的提示 -->
|
<!-- 拖拽时的提示 -->
|
||||||
<div v-if="isDragging" class="drag-hint">拖动调整</div>
|
<div v-if="isDragging" class="drag-hint">拖动调整</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,31 +40,125 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { computed, nextTick, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vitepress'
|
||||||
|
import {
|
||||||
|
createReadingBookmark,
|
||||||
|
readReadingBookmark,
|
||||||
|
writeReadingBookmark
|
||||||
|
} from '../utils/readingBookmark.js'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
const showProgress = ref(false)
|
const showProgress = ref(false)
|
||||||
const showArrow = ref(false)
|
const showArrow = ref(false)
|
||||||
|
const articleTitle = ref('')
|
||||||
|
const activeSection = ref('')
|
||||||
|
const restoredBookmark = ref(null)
|
||||||
// Circle circumference = 2 * PI * r, where r=24
|
// Circle circumference = 2 * PI * r, where r=24
|
||||||
const circumference = 2 * Math.PI * 24
|
const circumference = 2 * Math.PI * 24
|
||||||
let scrollTimer = null
|
let scrollTimer = null
|
||||||
|
let saveTimer = null
|
||||||
|
let restoreTimer = null
|
||||||
|
let clickSaveTimer = null
|
||||||
|
|
||||||
// 拖拽相关状态
|
// 拖拽相关状态
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const startY = ref(0)
|
const startY = ref(0)
|
||||||
const startProgress = ref(0)
|
const startProgress = ref(0)
|
||||||
|
const movedDuringDrag = ref(false)
|
||||||
let dragRafId = null
|
let dragRafId = null
|
||||||
|
let skipNextClick = false
|
||||||
|
|
||||||
|
const currentPath = () =>
|
||||||
|
`${window.location.pathname}${window.location.search || ''}`
|
||||||
|
|
||||||
|
const getClientStorage = () => {
|
||||||
|
try {
|
||||||
|
return window.localStorage
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxScrollY = () =>
|
||||||
|
Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
|
||||||
|
|
||||||
|
const getArticleTitle = () => {
|
||||||
|
const heading = document.querySelector('.vp-doc h1')
|
||||||
|
return (heading?.textContent || document.title || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateActiveSection = () => {
|
||||||
|
const headings = Array.from(
|
||||||
|
document.querySelectorAll('.vp-doc h2, .vp-doc h3')
|
||||||
|
)
|
||||||
|
let current = ''
|
||||||
|
|
||||||
|
for (const heading of headings) {
|
||||||
|
if (heading.getBoundingClientRect().top <= 96) {
|
||||||
|
current = heading.textContent?.trim() || ''
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSection.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkLabel = computed(() => {
|
||||||
|
const title = articleTitle.value || restoredBookmark.value?.title || ''
|
||||||
|
const section = activeSection.value || restoredBookmark.value?.section || ''
|
||||||
|
return section || title
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookmarkTitle = computed(() => {
|
||||||
|
const title =
|
||||||
|
articleTitle.value || restoredBookmark.value?.title || '当前文章'
|
||||||
|
const section = activeSection.value || restoredBookmark.value?.section || ''
|
||||||
|
return section ? `${title} - ${section}` : title
|
||||||
|
})
|
||||||
|
|
||||||
|
const progressTitle = computed(() =>
|
||||||
|
isDragging.value
|
||||||
|
? '拖动调整位置'
|
||||||
|
: `${bookmarkTitle.value} · 阅读进度 ${progress.value}%`
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveBookmark = () => {
|
||||||
|
writeReadingBookmark(
|
||||||
|
getClientStorage(),
|
||||||
|
createReadingBookmark({
|
||||||
|
path: currentPath(),
|
||||||
|
title: articleTitle.value,
|
||||||
|
section: activeSection.value,
|
||||||
|
scrollY: window.scrollY,
|
||||||
|
progress: progress.value
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleBookmarkSave = () => {
|
||||||
|
if (saveTimer) {
|
||||||
|
window.clearTimeout(saveTimer)
|
||||||
|
}
|
||||||
|
saveTimer = window.setTimeout(saveBookmark, 180)
|
||||||
|
}
|
||||||
|
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
// 拖拽时不更新进度,避免冲突
|
// 拖拽时不更新进度,避免冲突
|
||||||
if (isDragging.value) return
|
if (isDragging.value) return
|
||||||
|
|
||||||
|
articleTitle.value = getArticleTitle()
|
||||||
|
updateActiveSection()
|
||||||
|
|
||||||
const scrollTop = window.scrollY
|
const scrollTop = window.scrollY
|
||||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight
|
const docHeight = getMaxScrollY()
|
||||||
const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0
|
const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0
|
||||||
|
|
||||||
progress.value = Math.min(Math.round(scrollPercent), 100)
|
progress.value = Math.min(Math.round(scrollPercent), 100)
|
||||||
showProgress.value = scrollTop > 0 // 开始滚动就显示
|
showProgress.value = scrollTop > 0 // 开始滚动就显示
|
||||||
|
restoredBookmark.value = null
|
||||||
|
|
||||||
// 滚动时显示百分比
|
// 滚动时显示百分比
|
||||||
showArrow.value = false
|
showArrow.value = false
|
||||||
@@ -76,6 +174,55 @@ const updateProgress = () => {
|
|||||||
showArrow.value = true
|
showArrow.value = true
|
||||||
}
|
}
|
||||||
}, 1500)
|
}, 1500)
|
||||||
|
|
||||||
|
scheduleBookmarkSave()
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreBookmark = async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (restoreTimer) {
|
||||||
|
window.clearTimeout(restoreTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreTimer = window.setTimeout(() => {
|
||||||
|
articleTitle.value = getArticleTitle()
|
||||||
|
updateActiveSection()
|
||||||
|
|
||||||
|
const saved = readReadingBookmark(
|
||||||
|
getClientStorage(),
|
||||||
|
currentPath(),
|
||||||
|
getMaxScrollY()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!saved || saved.scrollY <= 0) {
|
||||||
|
updateProgress()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredBookmark.value = saved
|
||||||
|
articleTitle.value = saved.title || articleTitle.value
|
||||||
|
activeSection.value = saved.section || activeSection.value
|
||||||
|
progress.value = saved.progress
|
||||||
|
showProgress.value = true
|
||||||
|
showArrow.value = true
|
||||||
|
|
||||||
|
window.scrollTo({
|
||||||
|
top: saved.scrollY,
|
||||||
|
behavior: 'auto'
|
||||||
|
})
|
||||||
|
|
||||||
|
window.setTimeout(updateProgress, 0)
|
||||||
|
}, 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetRouteState = () => {
|
||||||
|
progress.value = 0
|
||||||
|
showProgress.value = false
|
||||||
|
showArrow.value = false
|
||||||
|
restoredBookmark.value = null
|
||||||
|
articleTitle.value = ''
|
||||||
|
activeSection.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始拖拽
|
// 开始拖拽
|
||||||
@@ -85,6 +232,7 @@ const startDrag = (e) => {
|
|||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
startY.value = 'touches' in e ? e.touches[0].clientY : e.clientY
|
startY.value = 'touches' in e ? e.touches[0].clientY : e.clientY
|
||||||
startProgress.value = progress.value
|
startProgress.value = progress.value
|
||||||
|
movedDuringDrag.value = false
|
||||||
|
|
||||||
// 添加全局事件监听
|
// 添加全局事件监听
|
||||||
document.addEventListener('mousemove', onDrag, { passive: false })
|
document.addEventListener('mousemove', onDrag, { passive: false })
|
||||||
@@ -100,6 +248,9 @@ const onDrag = (e) => {
|
|||||||
|
|
||||||
const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY
|
const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY
|
||||||
const deltaY = startY.value - currentY // 向上拖动为正值
|
const deltaY = startY.value - currentY // 向上拖动为正值
|
||||||
|
if (Math.abs(deltaY) > 4) {
|
||||||
|
movedDuringDrag.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// 每拖动 3 像素调整 1% 进度
|
// 每拖动 3 像素调整 1% 进度
|
||||||
const sensitivity = 3
|
const sensitivity = 3
|
||||||
@@ -130,6 +281,7 @@ const onDrag = (e) => {
|
|||||||
|
|
||||||
// 结束拖拽
|
// 结束拖拽
|
||||||
const endDrag = () => {
|
const endDrag = () => {
|
||||||
|
const shouldSkipClick = movedDuringDrag.value
|
||||||
isDragging.value = false
|
isDragging.value = false
|
||||||
|
|
||||||
// 清除事件监听
|
// 清除事件监听
|
||||||
@@ -147,22 +299,44 @@ const endDrag = () => {
|
|||||||
if (window.scrollY > 0) {
|
if (window.scrollY > 0) {
|
||||||
showArrow.value = true
|
showArrow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
articleTitle.value = getArticleTitle()
|
||||||
|
updateActiveSection()
|
||||||
|
saveBookmark()
|
||||||
|
|
||||||
|
if (shouldSkipClick) {
|
||||||
|
skipNextClick = true
|
||||||
|
window.setTimeout(() => {
|
||||||
|
skipNextClick = false
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击回到顶部
|
// 点击回到顶部
|
||||||
const handleClick = (e) => {
|
const handleClick = () => {
|
||||||
// 如果是拖拽结束后的点击,不触发回到顶部
|
// 如果是拖拽结束后的点击,不触发回到顶部
|
||||||
if (isDragging.value) return
|
if (isDragging.value || skipNextClick) {
|
||||||
|
skipNextClick = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (clickSaveTimer) {
|
||||||
|
window.clearTimeout(clickSaveTimer)
|
||||||
|
}
|
||||||
|
clickSaveTimer = window.setTimeout(() => {
|
||||||
|
updateProgress()
|
||||||
|
saveBookmark()
|
||||||
|
}, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('scroll', updateProgress, { passive: true })
|
window.addEventListener('scroll', updateProgress, { passive: true })
|
||||||
updateProgress()
|
restoreBookmark()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -170,6 +344,15 @@ onUnmounted(() => {
|
|||||||
if (scrollTimer) {
|
if (scrollTimer) {
|
||||||
clearTimeout(scrollTimer)
|
clearTimeout(scrollTimer)
|
||||||
}
|
}
|
||||||
|
if (saveTimer) {
|
||||||
|
clearTimeout(saveTimer)
|
||||||
|
}
|
||||||
|
if (restoreTimer) {
|
||||||
|
clearTimeout(restoreTimer)
|
||||||
|
}
|
||||||
|
if (clickSaveTimer) {
|
||||||
|
clearTimeout(clickSaveTimer)
|
||||||
|
}
|
||||||
// 清理拖拽事件
|
// 清理拖拽事件
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', endDrag)
|
document.removeEventListener('mouseup', endDrag)
|
||||||
@@ -179,6 +362,14 @@ onUnmounted(() => {
|
|||||||
cancelAnimationFrame(dragRafId)
|
cancelAnimationFrame(dragRafId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
() => {
|
||||||
|
resetRouteState()
|
||||||
|
restoreBookmark()
|
||||||
|
}
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -278,6 +469,35 @@ onUnmounted(() => {
|
|||||||
animation: bounce 1s ease-in-out infinite;
|
animation: bounce 1s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookmark-label {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 100%;
|
||||||
|
width: max-content;
|
||||||
|
max-width: min(260px, calc(100vw - 48px));
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--vp-c-bg);
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-progress:hover .bookmark-label {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes bounce {
|
@keyframes bounce {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
export const READING_BOOKMARK_VERSION = 1
|
||||||
|
export const READING_BOOKMARK_KEY_PREFIX = 'ev-reading-bookmark:'
|
||||||
|
|
||||||
|
export const getReadingBookmarkKey = (path) =>
|
||||||
|
`${READING_BOOKMARK_KEY_PREFIX}${path || '/'}`
|
||||||
|
|
||||||
|
const clampNumber = (value, min, max, fallback = min) => {
|
||||||
|
const numeric = Number(value)
|
||||||
|
if (!Number.isFinite(numeric)) return fallback
|
||||||
|
return Math.min(max, Math.max(min, numeric))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createReadingBookmark = ({
|
||||||
|
path,
|
||||||
|
title = '',
|
||||||
|
section = '',
|
||||||
|
scrollY = 0,
|
||||||
|
progress = 0,
|
||||||
|
now = () => Date.now()
|
||||||
|
}) => ({
|
||||||
|
version: READING_BOOKMARK_VERSION,
|
||||||
|
path: path || '/',
|
||||||
|
title: String(title || '').trim(),
|
||||||
|
section: String(section || '').trim(),
|
||||||
|
scrollY: Math.max(0, Math.round(Number(scrollY) || 0)),
|
||||||
|
progress: Math.round(clampNumber(progress, 0, 100, 0)),
|
||||||
|
updatedAt: now()
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeBookmark = (
|
||||||
|
value,
|
||||||
|
expectedPath,
|
||||||
|
maxScrollY = Number.MAX_SAFE_INTEGER
|
||||||
|
) => {
|
||||||
|
if (!value || typeof value !== 'object') return null
|
||||||
|
if (value.version !== READING_BOOKMARK_VERSION) return null
|
||||||
|
if (value.path !== expectedPath) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: READING_BOOKMARK_VERSION,
|
||||||
|
path: value.path,
|
||||||
|
title: String(value.title || '').trim(),
|
||||||
|
section: String(value.section || '').trim(),
|
||||||
|
scrollY: Math.round(
|
||||||
|
clampNumber(value.scrollY, 0, Math.max(0, maxScrollY), 0)
|
||||||
|
),
|
||||||
|
progress: Math.round(clampNumber(value.progress, 0, 100, 0)),
|
||||||
|
updatedAt: Number(value.updatedAt) || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readReadingBookmark = (storage, path, maxScrollY) => {
|
||||||
|
if (!storage) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expectedPath = path || '/'
|
||||||
|
const raw = storage.getItem(getReadingBookmarkKey(expectedPath))
|
||||||
|
if (!raw) return null
|
||||||
|
return normalizeBookmark(JSON.parse(raw), expectedPath, maxScrollY)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writeReadingBookmark = (storage, bookmark) => {
|
||||||
|
if (!storage || !bookmark?.path) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.setItem(
|
||||||
|
getReadingBookmarkKey(bookmark.path),
|
||||||
|
JSON.stringify(bookmark)
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createReadingBookmark,
|
||||||
|
getReadingBookmarkKey,
|
||||||
|
readReadingBookmark,
|
||||||
|
writeReadingBookmark
|
||||||
|
} from './readingBookmark.js'
|
||||||
|
|
||||||
|
const createStorage = () => {
|
||||||
|
const values = new Map()
|
||||||
|
|
||||||
|
return {
|
||||||
|
getItem(key) {
|
||||||
|
return values.has(key) ? values.get(key) : null
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
values.set(key, String(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('reading bookmarks', () => {
|
||||||
|
it('stores bookmarks by full path so each locale is independent', () => {
|
||||||
|
const storage = createStorage()
|
||||||
|
const zhBookmark = createReadingBookmark({
|
||||||
|
path: '/easy-vibe/zh-cn/stage-1/intro/',
|
||||||
|
title: '中文标题',
|
||||||
|
section: '小节',
|
||||||
|
scrollY: 320,
|
||||||
|
progress: 45,
|
||||||
|
now: () => 123
|
||||||
|
})
|
||||||
|
const enBookmark = createReadingBookmark({
|
||||||
|
path: '/easy-vibe/en/stage-1/intro/',
|
||||||
|
title: 'English title',
|
||||||
|
scrollY: 80,
|
||||||
|
progress: 12,
|
||||||
|
now: () => 456
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(writeReadingBookmark(storage, zhBookmark), true)
|
||||||
|
assert.equal(writeReadingBookmark(storage, enBookmark), true)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
readReadingBookmark(storage, '/easy-vibe/zh-cn/stage-1/intro/', 1000),
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
path: '/easy-vibe/zh-cn/stage-1/intro/',
|
||||||
|
title: '中文标题',
|
||||||
|
section: '小节',
|
||||||
|
scrollY: 320,
|
||||||
|
progress: 45,
|
||||||
|
updatedAt: 123
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
readReadingBookmark(storage, '/easy-vibe/en/stage-1/intro/', 1000)
|
||||||
|
.title,
|
||||||
|
'English title'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes invalid numeric values', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
createReadingBookmark({
|
||||||
|
path: '/easy-vibe/ja-jp/page/',
|
||||||
|
scrollY: -5,
|
||||||
|
progress: 140,
|
||||||
|
now: () => 1
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
path: '/easy-vibe/ja-jp/page/',
|
||||||
|
title: '',
|
||||||
|
section: '',
|
||||||
|
scrollY: 0,
|
||||||
|
progress: 100,
|
||||||
|
updatedAt: 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores malformed or mismatched stored values', () => {
|
||||||
|
const storage = createStorage()
|
||||||
|
|
||||||
|
storage.setItem(getReadingBookmarkKey('/easy-vibe/ko-kr/page/'), '{bad')
|
||||||
|
assert.equal(
|
||||||
|
readReadingBookmark(storage, '/easy-vibe/ko-kr/page/', 1000),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
storage.setItem(
|
||||||
|
getReadingBookmarkKey('/easy-vibe/ko-kr/page/'),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
path: '/easy-vibe/zh-cn/page/',
|
||||||
|
scrollY: 20,
|
||||||
|
progress: 10
|
||||||
|
})
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
readReadingBookmark(storage, '/easy-vibe/ko-kr/page/', 1000),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps restored scroll position to current document height', () => {
|
||||||
|
const storage = createStorage()
|
||||||
|
const path = '/easy-vibe/fr-fr/page/'
|
||||||
|
|
||||||
|
writeReadingBookmark(
|
||||||
|
storage,
|
||||||
|
createReadingBookmark({
|
||||||
|
path,
|
||||||
|
scrollY: 9000,
|
||||||
|
progress: 88,
|
||||||
|
now: () => 99
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(readReadingBookmark(storage, path, 640).scrollY, 640)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user