mirror of
https://github.com/gedoor/legado.git
synced 2024-07-06 23:47:49 +08:00
[skip ci] web: 重构章节内容及其进度
This commit is contained in:
parent
1ebf3491f4
commit
16c2791184
@ -4,7 +4,7 @@ import { ElMessage } from "element-plus/es";
|
||||
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/api */
|
||||
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */
|
||||
|
||||
const { hostname, port } = new URL(import.meta.env.VITE_API || location.href);
|
||||
const { hostname, port } = new URL(import.meta.env.VITE_API || location.origin);
|
||||
|
||||
const isSourecEditor = /source/i.test(location.href);
|
||||
const APIExceptionHandler = (error) => {
|
||||
|
@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="title" wordCount="0">{{ title }}</div>
|
||||
<div class="title" data-chapterpos="0" ref="titleRef">{{ title }}</div>
|
||||
<div
|
||||
v-for="(para, index) in contents"
|
||||
:key="index"
|
||||
:wordCount="wordCounts[index]"
|
||||
ref="paragraphRef"
|
||||
:data-chapterpos="chapterPos[index]"
|
||||
>
|
||||
<img
|
||||
class="full"
|
||||
@ -18,8 +19,10 @@
|
||||
|
||||
<script setup>
|
||||
import { getImageFromLegado, isLegadoUrl } from "@/plugins/utils";
|
||||
import jump from "@/plugins/jump";
|
||||
|
||||
const props = defineProps({
|
||||
chapterIndex: { type: Number, required: true },
|
||||
contents: { type: Array, required: true },
|
||||
title: { type: String, required: true },
|
||||
spacing: { type: Object, required: true },
|
||||
@ -43,8 +46,60 @@ const calculateWordCount = (paragraph) => {
|
||||
const imagePlaceHolder = " ";
|
||||
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length;
|
||||
};
|
||||
const wordCounts = computed(() => {
|
||||
return Array.from(props.contents, (content) => calculateWordCount(content));
|
||||
const chapterPos = computed(() => {
|
||||
let pos = -1;
|
||||
return Array.from(props.contents, (content) => {
|
||||
pos += calculateWordCount(content) + 1; //计算上一段的换行符
|
||||
return pos;
|
||||
});
|
||||
});
|
||||
|
||||
const titleRef = ref();
|
||||
const paragraphRef = ref();
|
||||
const scrollToReadedLength = (length) => {
|
||||
if (length === 0) return;
|
||||
console.log("已读长度", length);
|
||||
let paragraphIndex = chapterPos.value.findIndex(
|
||||
(wordCount) => wordCount >= length
|
||||
);
|
||||
if (paragraphIndex === -1) return;
|
||||
nextTick(() => {
|
||||
jump(paragraphRef.value[paragraphIndex], {
|
||||
duration: 0,
|
||||
});
|
||||
});
|
||||
};
|
||||
defineExpose({
|
||||
scrollToReadedLength,
|
||||
});
|
||||
const observer = ref(null);
|
||||
const emit = defineEmits(["readedLengthChange"]);
|
||||
onMounted(() => {
|
||||
observer.value = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (let { target, isIntersecting } of entries) {
|
||||
if (isIntersecting) {
|
||||
emit(
|
||||
"readedLengthChange",
|
||||
props.chapterIndex,
|
||||
parseInt(target.dataset.chapterpos)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
|
||||
}
|
||||
);
|
||||
observer.value.observe(titleRef.value);
|
||||
paragraphRef.value.forEach((element) => {
|
||||
observer.value.observe(element);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.value?.disconnect();
|
||||
observer.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -32,6 +32,23 @@ export const useBookStore = defineStore("book", {
|
||||
readSettingsVisible: false,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
bookProgress: (state) => {
|
||||
if (state.catalog.length == 0) return;
|
||||
// @ts-ignore
|
||||
const { index, chapterPos, bookName, bookAuthor } = state.readingBook;
|
||||
let title = state.catalog[index]?.title;
|
||||
if (!title) return;
|
||||
return {
|
||||
name: bookName,
|
||||
author: bookAuthor,
|
||||
durChapterIndex: index,
|
||||
durChapterPos: chapterPos,
|
||||
durChapterTime: new Date().getTime(),
|
||||
durChapterTitle: title,
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setConnectStatus(connectStatus) {
|
||||
this.connectStatus = connectStatus;
|
||||
@ -81,21 +98,9 @@ export const useBookStore = defineStore("book", {
|
||||
this.searchBooks = [];
|
||||
},
|
||||
//保存进度到app
|
||||
async saveBookProcess() {
|
||||
if (this.catalog.length == 0) return;
|
||||
// @ts-ignore
|
||||
const { index, chapterPos, bookName, bookAuthor } = this.readingBook;
|
||||
let title = this.catalog[index]?.title;
|
||||
if (!title) return;
|
||||
|
||||
API.saveBookProcess({
|
||||
name: bookName,
|
||||
author: bookAuthor,
|
||||
durChapterIndex: index,
|
||||
durChapterPos: chapterPos,
|
||||
durChapterTime: new Date().getTime(),
|
||||
durChapterTitle: title,
|
||||
});
|
||||
async saveBookProgress() {
|
||||
if (!this.bookProgress) return Promise.resolve();
|
||||
return API.saveBookProcess(this.bookProgress);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -88,11 +88,14 @@
|
||||
ref="chapter"
|
||||
>
|
||||
<chapter-content
|
||||
ref="chapterRef"
|
||||
:chapterIndex="data.index"
|
||||
:contents="data.content"
|
||||
:title="data.title"
|
||||
:spacing="store.config.spacing"
|
||||
:fontSize="fontSize"
|
||||
:fontFamily="fontFamily"
|
||||
@readedLengthChange="onReadedLengthChange"
|
||||
v-if="showContent"
|
||||
/>
|
||||
</div>
|
||||
@ -109,10 +112,10 @@ import settings from "@/plugins/config";
|
||||
import API from "@api";
|
||||
import loadingSvg from "@element-plus/icons-svg/loading.svg?raw";
|
||||
|
||||
// loading spinner
|
||||
const showLoading = ref(false);
|
||||
const loadingSerive = ref(null);
|
||||
const content = ref();
|
||||
|
||||
watch(showLoading, (loading) => {
|
||||
if (!loading) return loadingSerive.value?.close();
|
||||
loadingSerive.value = ElLoading.service({
|
||||
@ -132,13 +135,6 @@ try {
|
||||
localStorage.removeItem("config");
|
||||
}
|
||||
|
||||
const loading = ref();
|
||||
const noPoint = ref(true);
|
||||
const showToolBar = ref(false);
|
||||
const chapterData = ref([]);
|
||||
const scrollObserve = ref(null);
|
||||
const readingObserve = ref(null);
|
||||
|
||||
const {
|
||||
catalog,
|
||||
popCataVisible,
|
||||
@ -147,6 +143,7 @@ const {
|
||||
showContent,
|
||||
config,
|
||||
readingBook,
|
||||
bookProgress,
|
||||
} = storeToRefs(store);
|
||||
const chapterPos = computed({
|
||||
get: () => readingBook.value.chapterPos,
|
||||
@ -201,6 +198,7 @@ const chapterTheme = computed(() => {
|
||||
width: readWidth.value,
|
||||
};
|
||||
});
|
||||
const showToolBar = ref(false);
|
||||
const leftBarTheme = computed(() => {
|
||||
return {
|
||||
background: popupColor.value,
|
||||
@ -221,28 +219,25 @@ const rightBarTheme = computed(() => {
|
||||
});
|
||||
const isNight = computed(() => theme.value == 6);
|
||||
|
||||
watchEffect(() => {
|
||||
if (chapterData.value.length > 0) {
|
||||
store.setContentLoading(false);
|
||||
//添加章节内容到observe
|
||||
addReadingObserve();
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
document.title = catalog.value[chapterIndex.value]?.title || document.title;
|
||||
store.saveBookProcess();
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (!infiniteLoading.value) {
|
||||
scrollObserve.value?.disconnect();
|
||||
} else {
|
||||
scrollObserve.value?.observe(loading.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 顶部底部跳转
|
||||
const top = ref();
|
||||
const bottom = ref();
|
||||
const toTop = () => {
|
||||
jump(top.value);
|
||||
};
|
||||
const toBottom = () => {
|
||||
jump(bottom.value);
|
||||
};
|
||||
|
||||
// 书架路由切换
|
||||
const router = useRouter();
|
||||
const toShelf = () => {
|
||||
router.push("/");
|
||||
};
|
||||
|
||||
// 获取章节内容
|
||||
const chapterData = ref([]);
|
||||
const noPoint = ref(true);
|
||||
const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
||||
if (reloadChapter) {
|
||||
//展示进度条
|
||||
@ -252,6 +247,7 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
||||
jump(top.value, { duration: 0 });
|
||||
//从目录,按钮切换章节时保存进度 预加载时不保存
|
||||
saveReadingBookProgressToBrowser(index, chapterPos);
|
||||
chapterData.value = [];
|
||||
}
|
||||
let bookUrl = sessionStorage.getItem("bookUrl");
|
||||
let { title, index: chapterIndex } = catalog.value[index];
|
||||
@ -261,12 +257,12 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
||||
if (res.data.isSuccess) {
|
||||
let data = res.data.data;
|
||||
let content = data.split(/\n+/);
|
||||
updateChapterData({ index, content, title }, reloadChapter);
|
||||
chapterData.value.push({ index, content, title });
|
||||
if (reloadChapter) toChapterPos(chapterPos);
|
||||
} else {
|
||||
ElMessage({ message: res.data.errorMsg, type: "error" });
|
||||
let content = [res.data.errorMsg];
|
||||
updateChapterData({ index, content, title }, reloadChapter);
|
||||
chapterData.value.push({ index, content, title });
|
||||
}
|
||||
store.setContentLoading(true);
|
||||
showLoading.value = false;
|
||||
@ -279,55 +275,69 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
||||
(err) => {
|
||||
ElMessage({ message: "获取章节内容失败", type: "error" });
|
||||
let content = ["获取章节内容失败!"];
|
||||
updateChapterData({ index, content, title }, reloadChapter);
|
||||
chapterData.value.push({ index, content, title });
|
||||
showLoading.value = false;
|
||||
store.setShowContent(true);
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 章节进度跳转和计算
|
||||
const chapter = ref();
|
||||
const chapterRef = ref();
|
||||
const toChapterPos = (pos) => {
|
||||
nextTick(() => {
|
||||
let wordCount = 0;
|
||||
if (chapter.value.length != 1) return;
|
||||
for (let element of chapter.value[0].children) {
|
||||
wordCount += parseInt(element.getAttribute("wordCount")) + 1; //计算换行符
|
||||
if (wordCount - 1 >= pos) {
|
||||
//最后一段没有换行符
|
||||
jump(element, {
|
||||
duration: 0,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (chapterRef.value.length === 1)
|
||||
chapterRef.value[0].scrollToReadedLength(pos);
|
||||
});
|
||||
};
|
||||
//计算当前章节阅读的字数
|
||||
const computeChapterPos = () => {
|
||||
if (chapter.value.length == 0) return;
|
||||
//计算当前阅读进度对应的element
|
||||
let chapterElement = chapter.value.find(
|
||||
(element) => element.getAttribute("chapterIndex") == chapterIndex.value
|
||||
const onReadedLengthChange = (index, pos) => {
|
||||
saveReadingBookProgressToBrowser(index, pos);
|
||||
};
|
||||
|
||||
// 文档标题
|
||||
watchEffect(() => {
|
||||
document.title = catalog.value[chapterIndex.value]?.title || document.title;
|
||||
});
|
||||
|
||||
// 阅读记录保存浏览器
|
||||
const saveReadingBookProgressToBrowser = (index, pos) => {
|
||||
//保存localStorage
|
||||
let bookUrl = sessionStorage.getItem("bookUrl");
|
||||
var book = JSON.parse(localStorage.getItem(bookUrl));
|
||||
book.index = index;
|
||||
book.chapterPos = pos;
|
||||
localStorage.setItem(bookUrl, JSON.stringify(book));
|
||||
//最近阅读
|
||||
book = JSON.parse(localStorage.getItem("readingRecent"));
|
||||
book.chapterIndex = index;
|
||||
book.chapterPos = pos;
|
||||
localStorage.setItem("readingRecent", JSON.stringify(book));
|
||||
//保存vuex
|
||||
chapterIndex.value = index;
|
||||
chapterPos.value = pos;
|
||||
//保存sessionStorage
|
||||
sessionStorage.setItem("chapterIndex", index);
|
||||
sessionStorage.setItem("chapterPos", String(pos));
|
||||
};
|
||||
|
||||
// 进度同步
|
||||
// 返回同步 同步请求会在获取书架前完成
|
||||
// 刷新 关闭页面
|
||||
const syncBookProgress = () => {
|
||||
console.log("page hide");
|
||||
if (!bookProgress.value) return;
|
||||
// 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
|
||||
navigator.sendBeacon(
|
||||
`${import.meta.env.VITE_API || location.origin}/saveBookProgress`,
|
||||
JSON.stringify(bookProgress.value)
|
||||
);
|
||||
if (!chapterElement) return;
|
||||
//计算已读字数
|
||||
let mChapterPos = 0;
|
||||
for (let paragraph of chapterElement.children) {
|
||||
mChapterPos += parseInt(paragraph.getAttribute("wordCount")) + 1; //计算换行符
|
||||
if (paragraph.getBoundingClientRect().top >= 0) {
|
||||
chapterPos.value = mChapterPos - 1; //最后一段没有换行符
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const bottom = ref();
|
||||
const toTop = () => {
|
||||
jump(top.value);
|
||||
};
|
||||
const toBottom = () => {
|
||||
jump(bottom.value);
|
||||
};
|
||||
|
||||
// 定时同步
|
||||
|
||||
// 章节切换
|
||||
const toNextChapter = () => {
|
||||
store.setContentLoading(true);
|
||||
let index = chapterIndex.value + 1;
|
||||
@ -360,42 +370,33 @@ const toPreChapter = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
const saveReadingBookProgressToBrowser = (index, pos = chapterPos.value) => {
|
||||
//保存localStorage
|
||||
let bookUrl = sessionStorage.getItem("bookUrl");
|
||||
var book = JSON.parse(localStorage.getItem(bookUrl));
|
||||
book.index = index;
|
||||
book.chapterPos = pos;
|
||||
localStorage.setItem(bookUrl, JSON.stringify(book));
|
||||
//最近阅读
|
||||
book = JSON.parse(localStorage.getItem("readingRecent"));
|
||||
book.chapterIndex = index;
|
||||
book.chapterPos = pos;
|
||||
localStorage.setItem("readingRecent", JSON.stringify(book));
|
||||
//保存vuex
|
||||
chapterIndex.value = index;
|
||||
chapterPos.value = pos;
|
||||
//保存sessionStorage
|
||||
sessionStorage.setItem("chapterIndex", index);
|
||||
sessionStorage.setItem("chapterPos", String(pos));
|
||||
};
|
||||
const updateChapterData = async (data, reloadChapter) => {
|
||||
if (reloadChapter) {
|
||||
chapterData.value.splice(0);
|
||||
|
||||
// 无限滚动
|
||||
const scrollObserve = ref(null);
|
||||
const loading = ref();
|
||||
watchEffect(() => {
|
||||
if (!infiniteLoading.value) {
|
||||
scrollObserve.value?.disconnect();
|
||||
} else {
|
||||
scrollObserve.value?.observe(loading.value);
|
||||
}
|
||||
chapterData.value.push(data);
|
||||
};
|
||||
});
|
||||
const loadMore = () => {
|
||||
let index = chapterData.value.slice(-1)[0].index;
|
||||
if (catalog.value.length - 1 > index) {
|
||||
getContent(index + 1, false);
|
||||
}
|
||||
};
|
||||
const router = useRouter();
|
||||
const toShelf = () => {
|
||||
router.push("/");
|
||||
// IntersectionObserver回调 底部加载
|
||||
const handleIScrollObserve = (entries) => {
|
||||
if (showLoading.value) return;
|
||||
for (let { isIntersecting } of entries) {
|
||||
if (!isIntersecting) return;
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
//监听方向键
|
||||
|
||||
// 监听方向键
|
||||
const handleKeyPress = (event) => {
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
@ -438,52 +439,6 @@ const handleKeyPress = (event) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
//IntersectionObserver回调 底部加载
|
||||
const handleIScrollObserve = (entries) => {
|
||||
if (showLoading.value) return;
|
||||
for (let { isIntersecting } of entries) {
|
||||
if (!isIntersecting) return;
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
//IntersectionObserver回调 当前阅读章节序号
|
||||
const handleIReadingObserve = (entries) => {
|
||||
nextTick(() => {
|
||||
for (let { isIntersecting, target, boundingClientRect } of entries) {
|
||||
let chapterTitleIndex = parseInt(target.getAttribute("chapterIndex"));
|
||||
if (isIntersecting) {
|
||||
chapterIndex.value = chapterTitleIndex;
|
||||
} else {
|
||||
if (boundingClientRect.top < 0) {
|
||||
chapterIndex.value = chapterTitleIndex + 1;
|
||||
} else {
|
||||
chapterIndex.value = chapterTitleIndex - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
//添加所有章节到observe
|
||||
const addReadingObserve = () => {
|
||||
nextTick(() => {
|
||||
let chapterElements = chapter.value;
|
||||
if (!chapterElements) return;
|
||||
chapterElements.forEach((el) => readingObserve.value.observe(el));
|
||||
});
|
||||
};
|
||||
|
||||
onBeforeRouteLeave((to, from, next) => {
|
||||
computeChapterPos();
|
||||
saveReadingBookProgressToBrowser(chapterIndex.value);
|
||||
next();
|
||||
});
|
||||
/*
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
alert(111);
|
||||
});
|
||||
*/
|
||||
|
||||
onMounted(() => {
|
||||
showLoading.value = true;
|
||||
@ -523,13 +478,12 @@ onMounted(() => {
|
||||
|
||||
getContent(chapterIndex, true, chapterPos);
|
||||
window.addEventListener("keyup", handleKeyPress);
|
||||
window.addEventListener("visibilitychange", syncBookProgress);
|
||||
//监听底部加载
|
||||
scrollObserve.value = new IntersectionObserver(handleIScrollObserve, {
|
||||
rootMargin: "-100% 0% 20% 0%",
|
||||
});
|
||||
infiniteLoading.value && scrollObserve.value.observe(loading.value);
|
||||
//监听当前阅读章节
|
||||
readingObserve.value = new IntersectionObserver(handleIReadingObserve);
|
||||
//第二次点击同一本书 页面标题不会变化
|
||||
document.title = null;
|
||||
document.title = bookName + " | " + catalog.value[chapterIndex].title;
|
||||
@ -544,10 +498,10 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keyup", handleKeyPress);
|
||||
window.removeEventListener("visibilitychange", syncBookProgress);
|
||||
readSettingsVisible.value = false;
|
||||
popCataVisible.value = false;
|
||||
scrollObserve.value?.disconnect();
|
||||
readingObserve.value?.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -187,7 +187,7 @@ const toDetail = (bookUrl, bookName, bookAuthor, chapterIndex, chapterPos) => {
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(() => {
|
||||
//获取最近阅读书籍
|
||||
let readingRecentStr = localStorage.getItem("readingRecent");
|
||||
if (readingRecentStr != null) {
|
||||
@ -197,8 +197,10 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
showLoading.value = true;
|
||||
await store.saveBookProcess();
|
||||
fetchBookShelfData();
|
||||
store
|
||||
.saveBookProgress()
|
||||
//确保各种网络情况下同步请求先完成
|
||||
.finally(fetchBookShelfData);
|
||||
});
|
||||
const fetchBookShelfData = () => {
|
||||
API.getBookShelf()
|
||||
|
Loading…
Reference in New Issue
Block a user