feat: update version to 1.9.8 and enhance cloud sync functionality

This commit is contained in:
troyeguo
2025-05-18 11:52:33 +08:00
parent 21d35d145a
commit 0ec0b96e67
24 changed files with 222 additions and 121 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "koodo-reader",
"main": "main.js",
"version": "1.9.7",
"version": "1.9.8",
"description": "Koodo Reader is a cross-platform ebook reader",
"author": {
"name": "App by Troye",

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -341,6 +341,7 @@
"Beta pharse": "内测期间",
"Trial user": "试用用户",
"Paid user": "付费用户",
"Pro user": "专业版用户",
"Free user": "免费用户",
"Get device identifier": "获取设备标识符",
"Sync failed": "同步失败",
@@ -588,7 +589,9 @@
"Reset": "重置",
"Pro version": "升级专业版",
"Upgrade to Pro": "升级专业版",
"To ensure a smooth synchronization experience, your reading progress, notes, highlights, bookmarks, and other data will be stored and synced through our cloud service": "为了确保流畅的同步体验,您的阅读进度、笔记、高亮、书签等数据将通过我们的云服务进行存储和同步",
"Enable this option to increase synchronization speed. Your reading progress, notes, highlights, bookmarks, and other reading-related data will be stored and synced via our cloud service. Turning off this option will remove the above data from our cloud.": "启用此选项以提高同步速度。您的阅读进度、笔记、高亮、书签等阅读相关数据将通过我们的云服务进行存储和同步。关闭此选项将从我们的云端删除上述数据",
"Access may be unstable in China": "国内网络访问可能不稳定",
"Only WebDAV service provided by Alist is directly supported in Browser, Other WebDAV services need to enable CORS to work properly": "由于浏览器的限制,仅支持 Alist 提供的 WebDAV 服务,其他 WebDAV 服务需要开启 CORS 才能正常使用",
"Enable Koodo Sync": "启用 Koodo Sync",
"Disable automatic sync": "禁用自动同步",
"By default, Koodo Reader will automatically synchronize your data when you open the app and exit reading": "默认情况下Koodo Reader 会在您打开应用和退出阅读时自动同步数据",

View File

@@ -106,7 +106,7 @@
/* .dict-container li div {
display: inline;
} */
.dict-container ul {
/* .dict-container ul {
counter-reset: my-counter;
}
@@ -117,6 +117,37 @@
.dict-container li::before {
content: counter(my-counter) ". ";
font-weight: bold;
} */
/* 在 popupDict.css 中添加这些规则,它们会覆盖 reset.css 中的全局规则 */
.dict-container ul {
list-style-type: disc !important; /* 实心圆点 */
padding-left: 2em !important; /* 添加缩进 */
margin: 1em 0 !important; /* 添加上下边距 */
}
.dict-container ol {
list-style-type: decimal !important; /* 数字编号 */
padding-left: 2em !important; /* 添加缩进 */
margin: 1em 0 !important; /* 添加上下边距 */
}
.dict-container li {
list-style: inherit !important; /* 继承父元素的列表样式 */
display: list-item !important; /* 显示为列表项 */
margin: 0.5em 0 !important; /* 添加上下边距 */
}
/* 定义嵌套列表的样式 */
.dict-container ul ul {
list-style-type: circle !important; /* 二级列表使用空心圆点 */
}
.dict-container ul ul ul {
list-style-type: square !important; /* 三级列表使用方块 */
}
.dict-container ol ol {
list-style-type: lower-alpha !important; /* 二级有序列表使用小写字母 */
}
.audio-player {
width: 100px;

View File

@@ -4,10 +4,9 @@ export const driveList = [
value: "webdav",
icon: "icon-webdav",
isPro: false,
support: ["desktop", "phone"],
support: ["desktop", "browser", "phone"],
scoped: false,
},
{
label: "Dropbox",
value: "dropbox",

View File

@@ -82,7 +82,7 @@ export const syncSettingList = [
{
isElectron: false,
title: "Enable Koodo Sync",
desc: "To ensure a smooth synchronization experience, your reading progress, notes, highlights, bookmarks, and other data will be stored and synced through our cloud service",
desc: "Enable this option to increase synchronization speed. Your reading progress, notes, highlights, bookmarks, and other reading-related data will be stored and synced via our cloud service. Turning off this option will remove the above data from our cloud.",
propName: "isEnableKoodoSync",
},
{

View File

@@ -114,6 +114,21 @@ class Header extends React.Component<HeaderProps, HeaderState> {
this.props.handleFetchNotes();
this.props.handleFetchBookmarks();
});
this.props.handleCloudSyncFunc(this.handleCloudSync);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
if (ConfigService.getItem("isFinshReading") === "yes") {
ConfigService.setItem("isFinshReading", "no");
if (
this.state.isAutoSync &&
ConfigService.getItem("defaultSyncOption")
) {
this.setState({ isSync: true });
this.handleCloudSync();
}
}
}
});
}
async UNSAFE_componentWillReceiveProps(
nextProps: Readonly<HeaderProps>,
@@ -136,11 +151,10 @@ class Header extends React.Component<HeaderProps, HeaderState> {
console.error(error);
}
}
if (this.state.isAutoSync) {
if (this.state.isAutoSync && ConfigService.getItem("defaultSyncOption")) {
this.setState({ isSync: true });
await this.handleCloudSync();
}
nextProps.handleFetchUserConfig();
}
if (!nextProps.isAuthed && nextProps.isAuthed !== this.props.isAuthed) {
if (isElectron) {
@@ -338,9 +352,9 @@ class Header extends React.Component<HeaderProps, HeaderState> {
if (ConfigService.getReaderConfig("isEnableKoodoSync") === "yes") {
ConfigUtil.updateSyncData();
}
setTimeout(() => {
this.props.history.push("/manager/home");
}, 1000);
// setTimeout(() => {
// this.props.history.push("/manager/home");
// }, 1000);
};
handleSync = async (compareResult) => {
try {

View File

@@ -13,7 +13,7 @@ import {
handleFetchDefaultSyncOption,
handleFetchLoginOptionList,
handleFetchDataSourceList,
handleFetchUserConfig,
handleCloudSyncFunc,
} from "../../store/actions";
import { stateType } from "../../store";
import Header from "./component";
@@ -46,7 +46,7 @@ const actionCreator = {
handleFetchDefaultSyncOption,
handleFetchLoginOptionList,
handleFetchDataSourceList,
handleFetchUserConfig,
handleCloudSyncFunc,
};
export default connect(
mapStateToProps,

View File

@@ -20,7 +20,6 @@ export interface HeaderProps extends RouteComponentProps<any> {
handleImportDialog: (isOpenImportDialog: boolean) => void;
handleFeedbackDialog: (isShow: boolean) => void;
handleFetchAuthed: () => void;
handleFetchUserConfig: () => void;
handleFetchDefaultSyncOption: () => void;
handleFetchLoginOptionList: () => void;
handleFetchDataSourceList: () => void;
@@ -29,6 +28,9 @@ export interface HeaderProps extends RouteComponentProps<any> {
t: (title: string) => string;
handleFetchNotes: () => void;
handleFetchBookmarks: () => void;
handleCloudSyncFunc: (
cloudSyncFunc: () => Promise<false | undefined>
) => void;
}
export interface HeaderState {

View File

@@ -75,8 +75,9 @@ class OperationPanel extends React.Component<
ConfigService.setReaderConfig("isFullscreen", "yes");
}
}
handleExit() {
async handleExit() {
ConfigService.setReaderConfig("isFullscreen", "no");
ConfigService.setItem("isFinshReading", "yes");
this.props.handleReadingState(false);
this.props.handleSearch(false);
window.speechSynthesis && window.speechSynthesis.cancel();
@@ -85,7 +86,6 @@ class OperationPanel extends React.Component<
if (this.props.htmlBook) {
this.props.handleHtmlBook(null);
}
if (isElectron) {
if (ConfigService.getReaderConfig("isOpenInMain") === "yes") {
window.require("electron").ipcRenderer.invoke("exit-tab", "ping");

View File

@@ -21,7 +21,6 @@ export interface OperationPanelProps extends RouteComponentProps<any> {
handleOpenMenu: (isOpenMenu: boolean) => void;
handleShowBookmark: (isShowBookmark: boolean) => void;
handleReadingBook: (currentBook: BookModel | object) => void;
t: (title: string) => string;
handleHtmlBook: (htmlBook: HtmlBookModel | null) => void;
}

View File

@@ -647,7 +647,7 @@ class AccountSetting extends React.Component<
{this.props.userInfo.type === "trial"
? "Trial user"
: this.props.userInfo.type === "pro"
? "Paid user"
? "Pro user"
: "Free user"}
</Trans>
<>

View File

@@ -11,6 +11,7 @@ import toast from "react-hot-toast";
import {
handleContextMenu,
openExternalUrl,
testConnection,
WEBSITE_URL,
} from "../../../utils/common";
import { getStorageLocation } from "../../../utils/common";
@@ -89,12 +90,13 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
this.handleRest(this.state[stateName]);
};
handleAddDataSource = (event: any) => {
if (!event.target.value) {
let targetDrive = event.target.value;
if (!targetDrive) {
return;
}
if (
!driveList
.find((item) => item.value === event.target.value)
.find((item) => item.value === targetDrive)
?.support.includes("browser") &&
!isElectron
) {
@@ -106,14 +108,14 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
return;
}
if (
driveList.find((item) => item.value === event.target.value)?.isPro &&
driveList.find((item) => item.value === targetDrive)?.isPro &&
!this.props.isAuthed
) {
toast(this.props.t("This feature is not available in the free version"));
return;
}
this.props.handleSettingDrive(event.target.value);
let settingDrive = event.target.value;
this.props.handleSettingDrive(targetDrive);
let settingDrive = targetDrive;
if (
settingDrive === "dropbox" ||
settingDrive === "google" ||
@@ -126,20 +128,25 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
}
};
handleDeleteDataSource = async (event: any) => {
if (!event.target.value) {
let targetDrive = event.target.value;
if (!targetDrive) {
return;
}
await TokenService.setToken(event.target.value + "_token", "");
SyncService.removeSyncUtil(event.target.value);
removeCloudConfig(event.target.value);
await TokenService.setToken(targetDrive + "_token", "");
SyncService.removeSyncUtil(targetDrive);
removeCloudConfig(targetDrive);
if (isElectron) {
const { ipcRenderer } = window.require("electron");
await ipcRenderer.invoke("cloud-close", {
service: event.target.value,
service: targetDrive,
});
}
ConfigService.deleteListConfig(event.target.value, "dataSourceList");
ConfigService.deleteListConfig(targetDrive, "dataSourceList");
this.props.handleFetchDataSourceList();
if (targetDrive === ConfigService.getItem("defaultSyncOption")) {
ConfigService.removeItem("defaultSyncOption");
this.props.handleFetchDefaultSyncOption();
}
toast.success(this.props.t("Deletion successful"));
};
handleSetDefaultSyncOption = (event: any) => {
@@ -328,10 +335,37 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
/>
</>
)}
{this.props.settingDrive === "webdav" && !isElectron && (
<div
className="token-dialog-tip"
style={{
marginTop: "10px",
fontSize: "13px",
lineHeight: "16px",
color: "rgba(231, 69, 69, 0.8)",
}}
>
{this.props.t(
"Only WebDAV service provided by Alist is directly supported in Browser, Other WebDAV services need to enable CORS to work properly"
)}
</div>
)}
<div className="token-dialog-button-container">
<div
className="voice-add-confirm"
onClick={async () => {
if (
this.props.settingDrive === "webdav" ||
this.props.settingDrive === "s3compatible"
) {
let result = await testConnection(
this.props.settingDrive,
this.state.driveConfig
);
if (!result) {
return;
}
}
this.handleConfirmDrive();
}}
>
@@ -365,62 +399,24 @@ class SyncSetting extends React.Component<SettingInfoProps, SettingInfoState> {
<Trans>Authorize</Trans>
</div>
)}
{isElectron &&
(this.props.settingDrive === "webdav" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "sftp" ||
this.props.settingDrive === "mega" ||
this.props.settingDrive === "s3compatible") && (
<div
className="voice-add-confirm"
style={{ marginRight: "10px" }}
onClick={async () => {
toast.loading(this.props.t("Testing connection..."), {
id: "testing-connection-id",
});
const { ipcRenderer } = window.require("electron");
const fs = window.require("fs");
fs.writeFileSync(
getStorageLocation() + "/config/test.txt",
"Hello world!"
);
let driveConfig: any = {};
for (let item in this.state.driveConfig) {
driveConfig[item] = this.state.driveConfig[item];
}
let result = await ipcRenderer.invoke("cloud-upload", {
...driveConfig,
fileName: "test.txt",
service: this.props.settingDrive,
type: "config",
storagePath: getStorageLocation(),
isUseCache: false,
});
if (result) {
toast.success(this.props.t("Connection successful"), {
id: "testing-connection-id",
});
await ipcRenderer.invoke("cloud-delete", {
...driveConfig,
fileName: "test.txt",
service: this.props.settingDrive,
type: "config",
storagePath: getStorageLocation(),
isUseCache: false,
});
} else {
toast.error(this.props.t("Connection failed"), {
id: "testing-connection-id",
});
}
fs.unlinkSync(
getStorageLocation() + "/config/test.txt"
);
}}
>
<Trans>Test</Trans>
</div>
)}
{(this.props.settingDrive === "webdav" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "sftp" ||
this.props.settingDrive === "mega" ||
this.props.settingDrive === "s3compatible") && (
<div
className="voice-add-confirm"
style={{ marginRight: "10px" }}
onClick={async () => {
testConnection(
this.props.settingDrive,
this.state.driveConfig
);
}}
>
<Trans>Test</Trans>
</div>
)}
{(this.props.settingDrive === "webdav" ||
this.props.settingDrive === "ftp" ||
this.props.settingDrive === "s3compatible" ||

View File

@@ -401,6 +401,15 @@ class Login extends React.Component<LoginProps, LoginState> {
{this.props.t("Recommended (use with Nutstore)")}
</div>
)}
{ConfigService.getReaderConfig("lang") &&
ConfigService.getReaderConfig("lang").startsWith(
"zh"
) &&
item.value === "microsoft" && (
<div className="login-sync-text">
{this.props.t("Access may be unstable in China")}
</div>
)}
<div className="login-sync-subtitle">
<div>
{item.support.map((support) => {

View File

@@ -65,6 +65,9 @@ class Reader extends React.Component<ReaderProps, ReaderState> {
);
}
}, 1000);
window.addEventListener("beforeunload", function (event) {
ConfigService.setItem("isFinshReading", "yes");
});
}
UNSAFE_componentWillMount() {
let url = document.location.href;

View File

@@ -14,6 +14,9 @@ export function handleRenderBookFunc(renderBookFunc: () => void) {
export function handleImportBookFunc(importBookFunc: () => void) {
return { type: "HANDLE_IMPORT_BOOK_FUNC", payload: importBookFunc };
}
export function handleCloudSyncFunc(cloudSyncFunc: () => void) {
return { type: "HANDLE_CLOUD_SYNC_FUNC", payload: cloudSyncFunc };
}
export function handleRenderNoteFunc(renderNoteFunc: () => void) {
return { type: "HANDLE_RENDER_NOTE_FUNC", payload: renderNoteFunc };
}

View File

@@ -6,7 +6,7 @@ import BookModel from "../../models/Book";
import PluginModel from "../../models/Plugin";
import { Dispatch } from "redux";
import DatabaseService from "../../utils/storage/databaseService";
import { fetchUserConfig, fetchUserInfo } from "../../utils/request/user";
import { fetchUserInfo } from "../../utils/request/user";
import {
officialDictList,
officialTranList,
@@ -30,9 +30,6 @@ export function handleSearch(isSearch: boolean) {
export function handleUserInfo(userInfo: any) {
return { type: "HANDLE_USER_INFO", payload: userInfo };
}
export function handleUserConfig(userConfig: any) {
return { type: "HANDLE_USER_CONFIG", payload: userConfig };
}
export function handleDetailDialog(isDetailDialog: boolean) {
return { type: "HANDLE_DETAIL_DIALOG", payload: isDetailDialog };
}
@@ -115,6 +112,10 @@ export function handleFetchUserInfo() {
let userInfo: any = null;
if (response.code === 200) {
userInfo = response.data;
ConfigService.setReaderConfig(
"isEnableKoodoSync",
userInfo.is_enable_koodo_sync || "no"
);
}
if (
userInfo &&
@@ -125,21 +126,6 @@ export function handleFetchUserInfo() {
dispatch(handleUserInfo(userInfo));
};
}
export function handleFetchUserConfig() {
return async (dispatch: Dispatch) => {
let response = await fetchUserConfig();
let userConfig: any = null;
if (response.code === 200) {
userConfig = response.data;
console.log("UserConfig", userConfig);
ConfigService.setReaderConfig(
"isEnableKoodoSync",
userConfig.is_enable_koodo_sync
);
}
dispatch(handleUserConfig(userConfig));
};
}
export function handleFetchPlugins() {
return async (dispatch: Dispatch) => {
DatabaseService.getAllRecords("plugins").then((pluginList) => {

View File

@@ -47,7 +47,6 @@ export type stateType = {
isShowSupport: boolean;
isShowNew: boolean;
userInfo: any;
userConfig: any;
isAuthed: boolean;
isNewWarning: boolean;
isSelectBook: boolean;
@@ -72,6 +71,7 @@ export type stateType = {
currentBook: BookModel;
renderBookFunc: () => void;
importBookFunc: (file: any) => Promise<void>;
cloudSyncFunc: () => Promise<void>;
renderNoteFunc: () => void;
};
backupPage: {

View File

@@ -10,6 +10,7 @@ const initState = {
totalPage: 1,
renderBookFunc: () => {},
importBookFunc: () => {},
cloudSyncFunc: () => {},
renderNoteFunc: () => {},
};
export function book(
@@ -42,6 +43,11 @@ export function book(
...state,
importBookFunc: action.payload,
};
case "HANDLE_CLOUD_SYNC_FUNC":
return {
...state,
cloudSyncFunc: action.payload,
};
case "HANDLE_RENDER_NOTE_FUNC":
return {
...state,

View File

@@ -5,12 +5,16 @@ import {
BookHelper,
CommonTool,
ConfigService,
SyncUtil,
} from "../assets/lib/kookit-extra-browser.min";
import Book from "../models/Book";
import BookUtil from "./file/bookUtil";
import * as Kookit from "../assets/lib/kookit.min";
import DatabaseService from "./storage/databaseService";
import packageJson from "../../package.json";
import toast from "react-hot-toast";
import i18n from "../i18n";
import { getThirdpartyRequest } from "./request/thirdparty";
declare var window: any;
export const calculateFileMD5 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
@@ -494,3 +498,62 @@ export const checkMissingBook = (bookList: Book[]) => {
}
}
};
export const testConnection = async (driveName: string, driveConfig: any) => {
toast.loading(i18n.t("Testing connection..."), {
id: "testing-connection-id",
});
if (isElectron) {
const { ipcRenderer } = window.require("electron");
const fs = window.require("fs");
fs.writeFileSync(getStorageLocation() + "/config/test.txt", "Hello world!");
let result = await ipcRenderer.invoke("cloud-upload", {
...driveConfig,
fileName: "test.txt",
service: driveName,
type: "config",
storagePath: getStorageLocation(),
isUseCache: false,
});
if (result) {
toast.success(i18n.t("Connection successful"), {
id: "testing-connection-id",
});
await ipcRenderer.invoke("cloud-delete", {
...driveConfig,
fileName: "test.txt",
service: driveName,
type: "config",
storagePath: getStorageLocation(),
isUseCache: false,
});
} else {
toast.error(i18n.t("Connection failed"), {
id: "testing-connection-id",
});
}
fs.unlinkSync(getStorageLocation() + "/config/test.txt");
return result;
} else {
let thirdpartyRequest = await getThirdpartyRequest();
let syncUtil = new SyncUtil(driveName, driveConfig, thirdpartyRequest);
// 上传到云端
let result = await syncUtil.uploadFile(
"test.txt",
"config",
new Blob(["Hello world!"])
);
if (!result) {
toast.error(i18n.t("Connection failed"), {
id: "testing-connection-id",
});
return false;
} else {
toast.success(i18n.t("Connection successful"), {
id: "testing-connection-id",
});
}
// 删除云端文件
return await syncUtil.deleteFile("test.txt", "config");
}
};

View File

@@ -116,7 +116,6 @@ class ConfigUtil {
let response = await thirdpartyRequest.getSyncData();
if (response.code === 200) {
let syncData = response.data;
console.log("getSyncData", syncData);
this.syncData = syncData;
return JSON.parse(this.syncData[type] || defaultValue);
} else if (response.code === 401) {
@@ -130,8 +129,6 @@ class ConfigUtil {
}
}
static async updateSyncData() {
console.log("updateSyncData", this.updateData);
let thirdpartyRequest = await getThirdpartyRequest();
let response = await thirdpartyRequest.updateSyncData(this.updateData);
@@ -159,7 +156,6 @@ class ConfigUtil {
static async getCloudDatabase(database: string) {
if (ConfigService.getReaderConfig("isEnableKoodoSync") === "yes") {
let data = await this.getSyncData(database);
console.log("getCloudDatabase", database, data);
return data || [];
}
if (isElectron) {
@@ -210,7 +206,6 @@ class ConfigUtil {
return record;
});
}
console.log("uploadDatabase", type, data);
this.updateData[type] = JSON.stringify(data);
return;
}

View File

@@ -54,14 +54,6 @@ export const fetchUserInfo = async () => {
}
return response;
};
export const fetchUserConfig = async () => {
let userRequest = await getUserRequest();
let response = await userRequest.getUserConfig();
if (response.code === 401) {
handleExitApp();
}
return response;
};
export const updateUserConfig = async (config: any) => {
let userRequest = await getUserRequest();
let response = await userRequest.updateUserConfig(config);