[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/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) => {

View File

@ -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>

View File

@ -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);
},
},
});

View File

@ -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>

View File

@ -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()