[skip ci] web: 重构章节内容及其进度

This commit is contained in:
Xwite 2023-05-12 21:05:32 +08:00
parent 1ebf3491f4
commit 16c2791184
5 changed files with 183 additions and 167 deletions

View File

@ -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/api */
/** https://github.com/gedoor/legado/tree/master/app/src/main/java/io/legado/app/web */ /** 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 isSourecEditor = /source/i.test(location.href);
const APIExceptionHandler = (error) => { const APIExceptionHandler = (error) => {

View File

@ -1,9 +1,10 @@
<template> <template>
<div class="title" wordCount="0">{{ title }}</div> <div class="title" data-chapterpos="0" ref="titleRef">{{ title }}</div>
<div <div
v-for="(para, index) in contents" v-for="(para, index) in contents"
:key="index" :key="index"
:wordCount="wordCounts[index]" ref="paragraphRef"
:data-chapterpos="chapterPos[index]"
> >
<img <img
class="full" class="full"
@ -18,8 +19,10 @@
<script setup> <script setup>
import { getImageFromLegado, isLegadoUrl } from "@/plugins/utils"; import { getImageFromLegado, isLegadoUrl } from "@/plugins/utils";
import jump from "@/plugins/jump";
const props = defineProps({ const props = defineProps({
chapterIndex: { type: Number, required: true },
contents: { type: Array, required: true }, contents: { type: Array, required: true },
title: { type: String, required: true }, title: { type: String, required: true },
spacing: { type: Object, required: true }, spacing: { type: Object, required: true },
@ -43,8 +46,60 @@ const calculateWordCount = (paragraph) => {
const imagePlaceHolder = " "; const imagePlaceHolder = " ";
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length; return paragraph.replaceAll(imgPattern, imagePlaceHolder).length;
}; };
const wordCounts = computed(() => { const chapterPos = computed(() => {
return Array.from(props.contents, (content) => calculateWordCount(content)); 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> </script>

View File

@ -32,6 +32,23 @@ export const useBookStore = defineStore("book", {
readSettingsVisible: false, 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: { actions: {
setConnectStatus(connectStatus) { setConnectStatus(connectStatus) {
this.connectStatus = connectStatus; this.connectStatus = connectStatus;
@ -81,21 +98,9 @@ export const useBookStore = defineStore("book", {
this.searchBooks = []; this.searchBooks = [];
}, },
//保存进度到app //保存进度到app
async saveBookProcess() { async saveBookProgress() {
if (this.catalog.length == 0) return; if (!this.bookProgress) return Promise.resolve();
// @ts-ignore return API.saveBookProcess(this.bookProgress);
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,
});
}, },
}, },
}); });

View File

@ -88,11 +88,14 @@
ref="chapter" ref="chapter"
> >
<chapter-content <chapter-content
ref="chapterRef"
:chapterIndex="data.index"
:contents="data.content" :contents="data.content"
:title="data.title" :title="data.title"
:spacing="store.config.spacing" :spacing="store.config.spacing"
:fontSize="fontSize" :fontSize="fontSize"
:fontFamily="fontFamily" :fontFamily="fontFamily"
@readedLengthChange="onReadedLengthChange"
v-if="showContent" v-if="showContent"
/> />
</div> </div>
@ -109,10 +112,10 @@ import settings from "@/plugins/config";
import API from "@api"; import API from "@api";
import loadingSvg from "@element-plus/icons-svg/loading.svg?raw"; import loadingSvg from "@element-plus/icons-svg/loading.svg?raw";
// loading spinner
const showLoading = ref(false); const showLoading = ref(false);
const loadingSerive = ref(null); const loadingSerive = ref(null);
const content = ref(); const content = ref();
watch(showLoading, (loading) => { watch(showLoading, (loading) => {
if (!loading) return loadingSerive.value?.close(); if (!loading) return loadingSerive.value?.close();
loadingSerive.value = ElLoading.service({ loadingSerive.value = ElLoading.service({
@ -132,13 +135,6 @@ try {
localStorage.removeItem("config"); 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 { const {
catalog, catalog,
popCataVisible, popCataVisible,
@ -147,6 +143,7 @@ const {
showContent, showContent,
config, config,
readingBook, readingBook,
bookProgress,
} = storeToRefs(store); } = storeToRefs(store);
const chapterPos = computed({ const chapterPos = computed({
get: () => readingBook.value.chapterPos, get: () => readingBook.value.chapterPos,
@ -201,6 +198,7 @@ const chapterTheme = computed(() => {
width: readWidth.value, width: readWidth.value,
}; };
}); });
const showToolBar = ref(false);
const leftBarTheme = computed(() => { const leftBarTheme = computed(() => {
return { return {
background: popupColor.value, background: popupColor.value,
@ -221,28 +219,25 @@ const rightBarTheme = computed(() => {
}); });
const isNight = computed(() => theme.value == 6); 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 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) => { const getContent = (index, reloadChapter = true, chapterPos = 0) => {
if (reloadChapter) { if (reloadChapter) {
// //
@ -252,6 +247,7 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
jump(top.value, { duration: 0 }); jump(top.value, { duration: 0 });
// //
saveReadingBookProgressToBrowser(index, chapterPos); saveReadingBookProgressToBrowser(index, chapterPos);
chapterData.value = [];
} }
let bookUrl = sessionStorage.getItem("bookUrl"); let bookUrl = sessionStorage.getItem("bookUrl");
let { title, index: chapterIndex } = catalog.value[index]; let { title, index: chapterIndex } = catalog.value[index];
@ -261,12 +257,12 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
if (res.data.isSuccess) { if (res.data.isSuccess) {
let data = res.data.data; let data = res.data.data;
let content = data.split(/\n+/); let content = data.split(/\n+/);
updateChapterData({ index, content, title }, reloadChapter); chapterData.value.push({ index, content, title });
if (reloadChapter) toChapterPos(chapterPos); if (reloadChapter) toChapterPos(chapterPos);
} else { } else {
ElMessage({ message: res.data.errorMsg, type: "error" }); ElMessage({ message: res.data.errorMsg, type: "error" });
let content = [res.data.errorMsg]; let content = [res.data.errorMsg];
updateChapterData({ index, content, title }, reloadChapter); chapterData.value.push({ index, content, title });
} }
store.setContentLoading(true); store.setContentLoading(true);
showLoading.value = false; showLoading.value = false;
@ -279,55 +275,69 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
(err) => { (err) => {
ElMessage({ message: "获取章节内容失败", type: "error" }); ElMessage({ message: "获取章节内容失败", type: "error" });
let content = ["获取章节内容失败!"]; let content = ["获取章节内容失败!"];
updateChapterData({ index, content, title }, reloadChapter); chapterData.value.push({ index, content, title });
showLoading.value = false; showLoading.value = false;
store.setShowContent(true); store.setShowContent(true);
throw err; throw err;
} }
); );
}; };
//
const chapter = ref(); const chapter = ref();
const chapterRef = ref();
const toChapterPos = (pos) => { const toChapterPos = (pos) => {
nextTick(() => { nextTick(() => {
let wordCount = 0; if (chapterRef.value.length === 1)
if (chapter.value.length != 1) return; chapterRef.value[0].scrollToReadedLength(pos);
for (let element of chapter.value[0].children) {
wordCount += parseInt(element.getAttribute("wordCount")) + 1; //
if (wordCount - 1 >= pos) {
//
jump(element, {
duration: 0,
});
break;
}
}
}); });
}; };
// const onReadedLengthChange = (index, pos) => {
const computeChapterPos = () => { saveReadingBookProgressToBrowser(index, pos);
if (chapter.value.length == 0) return; };
//element
let chapterElement = chapter.value.find( //
(element) => element.getAttribute("chapterIndex") == chapterIndex.value 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 = () => { const toNextChapter = () => {
store.setContentLoading(true); store.setContentLoading(true);
let index = chapterIndex.value + 1; let index = chapterIndex.value + 1;
@ -360,42 +370,33 @@ const toPreChapter = () => {
}); });
} }
}; };
const saveReadingBookProgressToBrowser = (index, pos = chapterPos.value) => {
//localStorage //
let bookUrl = sessionStorage.getItem("bookUrl"); const scrollObserve = ref(null);
var book = JSON.parse(localStorage.getItem(bookUrl)); const loading = ref();
book.index = index; watchEffect(() => {
book.chapterPos = pos; if (!infiniteLoading.value) {
localStorage.setItem(bookUrl, JSON.stringify(book)); scrollObserve.value?.disconnect();
// } else {
book = JSON.parse(localStorage.getItem("readingRecent")); scrollObserve.value?.observe(loading.value);
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);
} }
chapterData.value.push(data); });
};
const loadMore = () => { const loadMore = () => {
let index = chapterData.value.slice(-1)[0].index; let index = chapterData.value.slice(-1)[0].index;
if (catalog.value.length - 1 > index) { if (catalog.value.length - 1 > index) {
getContent(index + 1, false); getContent(index + 1, false);
} }
}; };
const router = useRouter(); // IntersectionObserver
const toShelf = () => { const handleIScrollObserve = (entries) => {
router.push("/"); if (showLoading.value) return;
for (let { isIntersecting } of entries) {
if (!isIntersecting) return;
loadMore();
}
}; };
//
//
const handleKeyPress = (event) => { const handleKeyPress = (event) => {
switch (event.key) { switch (event.key) {
case "ArrowLeft": case "ArrowLeft":
@ -438,52 +439,6 @@ const handleKeyPress = (event) => {
break; 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(() => { onMounted(() => {
showLoading.value = true; showLoading.value = true;
@ -523,13 +478,12 @@ onMounted(() => {
getContent(chapterIndex, true, chapterPos); getContent(chapterIndex, true, chapterPos);
window.addEventListener("keyup", handleKeyPress); window.addEventListener("keyup", handleKeyPress);
window.addEventListener("visibilitychange", syncBookProgress);
// //
scrollObserve.value = new IntersectionObserver(handleIScrollObserve, { scrollObserve.value = new IntersectionObserver(handleIScrollObserve, {
rootMargin: "-100% 0% 20% 0%", rootMargin: "-100% 0% 20% 0%",
}); });
infiniteLoading.value && scrollObserve.value.observe(loading.value); infiniteLoading.value && scrollObserve.value.observe(loading.value);
//
readingObserve.value = new IntersectionObserver(handleIReadingObserve);
// //
document.title = null; document.title = null;
document.title = bookName + " | " + catalog.value[chapterIndex].title; document.title = bookName + " | " + catalog.value[chapterIndex].title;
@ -544,10 +498,10 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("keyup", handleKeyPress); window.removeEventListener("keyup", handleKeyPress);
window.removeEventListener("visibilitychange", syncBookProgress);
readSettingsVisible.value = false; readSettingsVisible.value = false;
popCataVisible.value = false; popCataVisible.value = false;
scrollObserve.value?.disconnect(); scrollObserve.value?.disconnect();
readingObserve.value?.disconnect();
}); });
</script> </script>

View File

@ -187,7 +187,7 @@ const toDetail = (bookUrl, bookName, bookAuthor, chapterIndex, chapterPos) => {
}); });
}; };
onMounted(async () => { onMounted(() => {
// //
let readingRecentStr = localStorage.getItem("readingRecent"); let readingRecentStr = localStorage.getItem("readingRecent");
if (readingRecentStr != null) { if (readingRecentStr != null) {
@ -197,8 +197,10 @@ onMounted(async () => {
} }
} }
showLoading.value = true; showLoading.value = true;
await store.saveBookProcess(); store
fetchBookShelfData(); .saveBookProgress()
//
.finally(fetchBookShelfData);
}); });
const fetchBookShelfData = () => { const fetchBookShelfData = () => {
API.getBookShelf() API.getBookShelf()