├── .eslintrc.json ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── README_CN.md ├── app ├── api │ ├── book │ │ ├── download │ │ │ └── route.ts │ │ └── route.ts │ ├── route.ts │ └── webdav │ │ ├── putFileContents │ │ └── route.ts │ │ └── route.ts ├── clientApi │ └── index.ts ├── components │ ├── ConnectModal.tsx │ ├── FileList.tsx │ ├── Reader.tsx │ ├── ServerTabs.tsx │ ├── ServerViewer.less │ ├── ServerViewer.tsx │ └── SeverTab.tsx ├── favicon.ico ├── globals.css ├── hook │ ├── actions │ │ └── useServerActions.tsx │ ├── query │ │ ├── useQueryBook.ts │ │ ├── useQueryDirectoryContents.ts │ │ ├── useQueryFileList.ts │ │ └── useSyncPagination.ts │ ├── useCachedBookContent.ts │ ├── useDeviceWidth.ts │ └── useSearchParamsUtil.ts ├── interface │ └── index.ts ├── layout.tsx ├── lib │ ├── axios.ts │ └── winston.ts ├── page.tsx ├── provider │ └── react-query-provider.tsx ├── store │ ├── index.ts │ ├── progressStore.ts │ ├── useAddServerModal.ts │ ├── useBooksContent.ts │ └── useServerViewerStore.ts ├── ui │ └── AlertDialog.tsx ├── utils │ ├── api.ts │ ├── cache.ts │ ├── index.ts │ └── searchParams.ts └── viewer │ └── [bookId] │ ├── components │ ├── Reader │ │ ├── hooks │ │ │ └── useRendition.ts │ │ └── index.tsx │ └── ReaderContainer │ │ ├── hooks │ │ ├── useHookClick.ts │ │ ├── useHookKeyPress.ts │ │ └── useHookLocationChanged.ts │ │ └── index.tsx │ ├── page.tsx │ ├── store │ └── index.ts │ └── types │ └── index.ts ├── combined.log ├── error.log ├── file.epub ├── file.txt ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | *.log 38 | combined.log 39 | error.log 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | 9 | "command": "npm run dev" 10 | }, 11 | { 12 | "name": "Next.js: debug client-side", 13 | "type": "chrome", 14 | "request": "launch", 15 | "url": "http://localhost:3000" 16 | }, 17 | { 18 | "name": "Next.js: debug full stack", 19 | "type": "node-terminal", 20 | "request": "launch", 21 | "command": "npm run dev", 22 | "serverReadyAction": { 23 | "pattern": "- Local:.+(https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EPUB Reader 2 | [切换至中文版](README_CN.md) 3 | 4 | This is a web application for reading EPUB ebooks built using Next.js. It allows users to access EPUB files stored on a WebDAV server, read the ebooks, and synchronize reading positions across devices. 5 | 6 | ## Key Features 7 | 8 | - WebDAV integration - Access EPUB files stored on a WebDAV server 9 | - EPUB reading - Read unencrypted EPUB format ebooks 10 | - Sync reading position - Synchronize last read position across devices 11 | 12 | ### Reading Experience 13 | 14 | The current page and position are synchronized in real-time between devices using configured authentication credentials. Simply open the same book on another device, and it will open to the current reading position. 15 | 16 | ## Build and Deployment 17 | 18 | This project uses Next.js and React. The recommended node version is 16+. 19 | 20 | ### Deploying to Vercel 21 | 22 | Click the button below to directly deploy the application to Vercel: 23 | 24 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/kmfb/repub) 25 | 26 | ## Contribution 27 | 28 | Contributions are welcome! Please open an issue or PR to report any bugs or improvements. -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # EPUB Reader 2 | 3 | [Change to English Version](README.md) 4 | 5 | 这是一个使用 Next.js 构建的 EPUB 电子书阅读器 Web 应用程序。它使用户能够从 WebDAV 服务器访问 EPUB 文件,阅读电子书,并在设备之间同步阅读位置。 6 | 7 | ## 主要功能 8 | 9 | - WebDAV 集成 - 访问存储在 WebDAV 服务器上的 EPUB 文件 10 | - EPUB 阅读 - 阅读未加密的 EPUB 格式电子书 11 | - 同步阅读位置 - 在设备之间同步上次阅读的位置 12 | 13 | ### 阅读体验 14 | 15 | 当前页面和位置使用配置的身份验证凭据在设备之间实时同步。只需在另一台设备上打开同一本书,它就会打开到当前的阅读位置。 16 | 17 | ## 构建和部署 18 | 19 | 这个项目使用 Next.js 和 React。推荐的 node 版本是 16+。 20 | 21 | ### 部署到 Vercel 22 | 23 | 点击下面的按钮将应用程序直接部署到 Vercel: 24 | 25 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/kmfb/repub) 26 | 27 | 28 | ## 贡献 29 | 30 | 欢迎贡献!请打开 issue 或 PR 以报告任何错误或改进。 -------------------------------------------------------------------------------- /app/api/book/download/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { AuthType, createClient } from "webdav"; 3 | import queryString from "query-string"; 4 | import { getClientConfigFromUrl } from "@/app/utils"; 5 | 6 | import _ from "lodash"; 7 | import { successResponse, withErrorHandler } from "@/app/utils/api"; 8 | import { IServerFormData } from "@/app/interface"; 9 | import { readFileSync } from "fs"; 10 | let fs = require("fs"); 11 | export const dynamic = "force-dynamic"; // defaults to force-static 12 | 13 | export interface IServerQueryObj 14 | extends Omit { 15 | url: string; 16 | path: string; 17 | } 18 | 19 | export const GET = withErrorHandler(async (request: NextRequest) => { 20 | const { url } = request; 21 | // JSON.parse(""); 22 | const config = getClientConfigFromUrl(url); 23 | const client = createClient(config.url, _.omit(config, ["url"])); 24 | 25 | const dUrl: any = client.getFileUploadLink(config.path); 26 | // Write stream to a file 27 | 28 | return successResponse(dUrl); 29 | }); 30 | -------------------------------------------------------------------------------- /app/api/book/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { AuthType, createClient } from "webdav"; 3 | import queryString from "query-string"; 4 | import { getClientConfigFromUrl } from "@/app/utils"; 5 | 6 | import _ from "lodash"; 7 | import { successResponse, withErrorHandler } from "@/app/utils/api"; 8 | import { IServerFormData } from "@/app/interface"; 9 | import { readFileSync } from "fs"; 10 | import JSZip from "jszip"; 11 | let fs = require("fs"); 12 | export const dynamic = "force-dynamic"; // defaults to force-static 13 | 14 | export interface IServerQueryObj 15 | extends Omit { 16 | url: string; 17 | path: string; 18 | } 19 | var blobStream = require("blob-stream"); 20 | export const GET = withErrorHandler( 21 | async (request: NextRequest, response: NextResponse) => { 22 | const { headers, url } = request; 23 | // JSON.parse(""); 24 | const parsed = queryString.parseUrl(url); 25 | const { query } = parsed; 26 | const config = getClientConfigFromUrl(url); 27 | const client = createClient(config.url, _.omit(config, ["url"])); 28 | 29 | const fileStream = client.createReadStream(config.path); 30 | const fileSize = query.fileSize ? query.fileSize : null; 31 | return new Response(fileStream as any, { 32 | headers: { 33 | "Content-Type": "application/zip", 34 | "Content-Disposition": "attachment; filename=download.zip", 35 | ...(fileSize && { 36 | "Content-Length": fileSize as string, 37 | }), 38 | }, 39 | }); 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /app/api/route.ts: -------------------------------------------------------------------------------- 1 | export const dynamic = 'force-dynamic' // defaults to force-static 2 | 3 | export async function GET(request: Request) { 4 | return new Response('Hello, Next.js!', { 5 | status: 200, 6 | headers: { 7 | 'Access-Control-Allow-Origin': '*', 8 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 9 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 10 | }, 11 | }) 12 | } -------------------------------------------------------------------------------- /app/api/webdav/putFileContents/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { AuthType, createClient } from "webdav"; 3 | import queryString from "query-string"; 4 | import { getClientConfigFromUrl, getConfigPath } from "@/app/utils"; 5 | 6 | import _ from "lodash"; 7 | import { successResponse, withErrorHandler } from "@/app/utils/api"; 8 | import { IServerFormData } from "@/app/interface"; 9 | export const dynamic = "force-dynamic"; // defaults to force-static 10 | 11 | export interface IServerQueryObj 12 | extends Omit { 13 | url: string; 14 | path: string; 15 | } 16 | 17 | export const POST = withErrorHandler(async (request: NextRequest) => { 18 | const { url } = request; 19 | // JSON.parse(""); 20 | const body = await request.json(); 21 | const config = getClientConfigFromUrl(url); 22 | const client = createClient(config.url, _.omit(config, ["url"])); 23 | 24 | const upload = async () => { 25 | const json = JSON.stringify(body); 26 | 27 | const buffer = Buffer.from(json); 28 | const configFilePath = getConfigPath(config as any, "index-storage.json"); 29 | const isSuccess = await client.putFileContents(configFilePath, buffer); 30 | 31 | return isSuccess; 32 | }; 33 | 34 | const isSuccess = await upload(); 35 | return successResponse({ 36 | isSuccess, 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /app/api/webdav/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { AuthType, createClient } from "webdav"; 3 | import queryString from "query-string"; 4 | import { getClientConfigFromUrl } from "@/app/utils"; 5 | 6 | import _ from "lodash"; 7 | import { successResponse, withErrorHandler } from "@/app/utils/api"; 8 | import { IServerFormData } from "@/app/interface"; 9 | export const dynamic = "force-dynamic"; // defaults to force-static 10 | 11 | export interface IServerQueryObj 12 | extends Omit { 13 | url: string; 14 | path: string; 15 | } 16 | 17 | export const GET = withErrorHandler(async (request: NextRequest) => { 18 | const { url } = request; 19 | // JSON.parse(""); 20 | const config = getClientConfigFromUrl(url); 21 | const client = createClient(config.url, _.omit(config, ["url"])); 22 | 23 | const getDirectoryItems = async () => { 24 | const directoryItems = await client.getDirectoryContents(config.path); 25 | 26 | return directoryItems; 27 | }; 28 | 29 | const dItems = await getDirectoryItems(); 30 | return successResponse(dItems); 31 | }); 32 | -------------------------------------------------------------------------------- /app/clientApi/index.ts: -------------------------------------------------------------------------------- 1 | import { IBook, IServerFormData } from "../interface"; 2 | import queryString from "query-string"; 3 | import axios from "../lib/axios"; 4 | import { IFile } from "../components/FileList"; 5 | import { queryClient } from "../provider/react-query-provider"; 6 | import { getBookId } from "../utils"; 7 | import { AxiosProgressEvent } from "axios"; 8 | 9 | import progressStore from "../store/progressStore"; 10 | import _ from "lodash"; 11 | 12 | export const getDirectoryContents = async ( 13 | path: string, 14 | server: IServerFormData 15 | ) => { 16 | const query = { 17 | path, 18 | ...server, 19 | }; 20 | const queryStr = queryString.stringify(query); 21 | return axios.get(`/webdav?${queryStr}`); 22 | }; 23 | 24 | export const getFile = async (file: IFile, server: IServerFormData) => { 25 | const query = { 26 | path: file.filename, 27 | fileSize: file.size, 28 | ...server, 29 | }; 30 | const queryStr = queryString.stringify(query); 31 | 32 | return axios.get(`/book?${queryStr}`, { 33 | responseType: "blob", 34 | onDownloadProgress: (progressEvent: AxiosProgressEvent) => { 35 | // console.log(progressEvent, "progressEvent"); 36 | 37 | const store = progressStore; 38 | const state: any = store.getState(); 39 | state.setBooks(getBookId(file, server), { 40 | ...progressEvent, 41 | }); 42 | }, 43 | }); 44 | }; 45 | 46 | export const getFileDownloadUrl = (file: IFile, server: IServerFormData) => { 47 | const query = { 48 | path: file.filename, 49 | ...server, 50 | }; 51 | const queryStr = queryString.stringify(query); 52 | return axios.get(`/book/download?${queryStr}`); 53 | }; 54 | 55 | export const syncStateToWebdav = async ( 56 | server: IServerFormData, 57 | books: Array 58 | ) => { 59 | 60 | 61 | return await axios.post( 62 | `/webdav/putFileContents?${queryString.stringify(server)}`, 63 | { 64 | books, 65 | } 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /app/components/ConnectModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Add, LockOpenOutlined, LockOutlined } from "@mui/icons-material"; 3 | import { 4 | Button, 5 | DialogContent, 6 | DialogTitle, 7 | FormControl, 8 | FormLabel, 9 | Input, 10 | Modal, 11 | ModalDialog, 12 | Select, 13 | Stack, 14 | Switch, 15 | Typography, 16 | Option, 17 | } from "@mui/joy"; 18 | import React, { useEffect } from "react"; 19 | import { Controller, useForm, useWatch } from "react-hook-form"; 20 | import { AuthType, createClient } from "webdav"; 21 | import useAddServerModal from "../store/useAddServerModal"; 22 | import useServerViewerStore from "../store/useServerViewerStore"; 23 | import { IServerFormData } from "../interface"; 24 | import { isHttps } from "../utils"; 25 | import { v4 as uuidv4 } from "uuid"; 26 | import _ from "lodash"; 27 | import useServerActions from "../hook/actions/useServerActions"; 28 | 29 | function ConnectModal() { 30 | const { updateCurrentServerAndPushToPath } = useServerActions(); 31 | const { open, setOpen, setCurrentEditServer, currentEditServer } = 32 | useAddServerModal(); 33 | const isEdit = !_.isEmpty(currentEditServer); 34 | 35 | const { addServerConfig, updateServerConfig } = useServerViewerStore(); 36 | const { register, handleSubmit, getValues, control, setValue } = 37 | useForm({ 38 | values: isEdit 39 | ? currentEditServer 40 | : { 41 | authType: AuthType.Password, 42 | protocol: "http", 43 | port: 31580, 44 | host: "www.stardusted.top", 45 | username: "kmfb", 46 | password: "1q2w3e*", 47 | }, 48 | }); 49 | 50 | const onSubmit = (data: IServerFormData) => { 51 | if (isEdit) { 52 | updateServerConfig(data, currentEditServer.id as any); 53 | updateCurrentServerAndPushToPath(data); 54 | } else { 55 | addServerConfig({ 56 | id: uuidv4(), 57 | ...data, 58 | }); 59 | } 60 | 61 | setOpen(false); 62 | }; 63 | 64 | useEffect(() => { 65 | if (!open) { 66 | setCurrentEditServer({} as any); 67 | } 68 | }, [open]); 69 | 70 | return ( 71 |
72 | 73 | setOpen(false)} disablePortal> 74 | 75 | 服务器配置 76 | 77 |
78 | 79 | 80 | 认证类型 81 | ( 88 | 107 | )} 108 | /> 109 | 110 | 111 | 协议 112 | ( 119 | 124 | 125 | <>https 126 | 127 | ) : ( 128 | <> 129 | 130 | <>http 131 | 132 | ) 133 | } 134 | checked={isHttps(value)} 135 | onChange={(e) => { 136 | onChange(e.target.checked ? "https" : "http"); 137 | }} 138 | /> 139 | )} 140 | /> 141 | {/* } 144 | endDecorator={} 145 | {...register("protocol")} 146 | /> */} 147 | 148 | 149 | Host 150 | 151 | 152 | 153 | Port 154 | 155 | 156 | 157 | Username 158 | 159 | 160 | 161 | Password 162 | 163 | 164 | 167 | 168 |
169 |
170 |
171 |
172 |
173 |
174 | ); 175 | } 176 | 177 | export default ConnectModal; 178 | -------------------------------------------------------------------------------- /app/components/FileList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | 3 | import { 4 | Description as DescriptionIcon, 5 | Folder as FolderIcon, 6 | } from "@mui/icons-material"; 7 | import { IServerFormData } from "../interface"; 8 | import useQueryBook from "../hook/query/useQueryBook"; 9 | 10 | import { useRouter, useSearchParams } from "next/navigation"; 11 | import { repubCache } from "../utils/cache"; 12 | import { getBookId, getFileNameByPath } from "../utils"; 13 | import useIndexStore from "../store"; 14 | import useQueryDirectoryContents from "../hook/query/useQueryDirectoryContents"; 15 | 16 | import useBooksContent from "../store/useBooksContent"; 17 | import { useQuery } from "@tanstack/react-query"; 18 | import _ from "lodash"; 19 | import { CircularProgress } from "@mui/joy"; 20 | import progressStore from "../store/progressStore"; 21 | import queryString from "query-string"; 22 | import useSearchParamsUtil from "../hook/useSearchParamsUtil"; 23 | export interface IFile { 24 | filename: string; 25 | basename: string; 26 | lastmod: string; 27 | size: number; 28 | type: "file" | "directory"; 29 | } 30 | 31 | interface FileListProps { 32 | files: IFile[]; 33 | server: IServerFormData; 34 | } 35 | 36 | const getFileIcon = (type: string) => { 37 | switch (type) { 38 | case "file": 39 | return ; 40 | case "directory": 41 | return ; 42 | default: 43 | return null; 44 | } 45 | }; 46 | const filenameClass = "ml-2 font-medium truncate"; 47 | const ProgressBar = (props: { file: IFile; server: IServerFormData }) => { 48 | const { file, server } = props; 49 | const bookId = getBookId(file, server); 50 | const { subscribe, getState } = progressStore; 51 | const states: any = getState(); 52 | const currentBook = states.books[bookId]; 53 | 54 | const [value, setValue] = useState( 55 | currentBook ? currentBook.progress * 100 : null 56 | ); 57 | 58 | subscribe((state: any, prev) => { 59 | if (!state.books[bookId]) return; 60 | const p: any = state.books[bookId].progress; 61 | setValue(p * 100); 62 | }); 63 | 64 | if (!_.isNumber(value)) { 65 | return null; 66 | } 67 | 68 | return ( 69 |
70 | 71 |
72 | ); 73 | }; 74 | 75 | const FileList: React.FC = ({ files, server }) => { 76 | const queryBookMutation = useQueryBook(); 77 | const directoryContentsMutation = useQueryDirectoryContents(); 78 | const router = useRouter(); 79 | const searchParamsUtil = useSearchParamsUtil(); 80 | const { addBook: addBookPersist } = useIndexStore(); 81 | const { addBook } = useBooksContent(); 82 | const searchParams = useSearchParams(); 83 | const { books } = useIndexStore(); 84 | const handleClickFile = async (file: IFile) => { 85 | if (file.type === "directory") { 86 | directoryContentsMutation.mutate({ 87 | path: file.filename, 88 | server, 89 | }); 90 | return; 91 | } 92 | const isEpub = file.filename.endsWith(".epub"); 93 | if (!isEpub) { 94 | return; 95 | } 96 | const bookId = getBookId(file, server); 97 | 98 | const cachedBookRes = await repubCache.read(bookId); 99 | 100 | if (!cachedBookRes) { 101 | queryBookMutation.mutate({ 102 | file, 103 | server, 104 | }); 105 | return; 106 | } 107 | const content = await cachedBookRes.blob(); 108 | const book = { 109 | id: bookId, 110 | name: file.filename, 111 | serverId: server.id as any, 112 | content, 113 | }; 114 | addBook(book); 115 | addBookPersist(book); 116 | router.push(`/viewer/${bookId}`); 117 | }; 118 | 119 | const currentPath = searchParamsUtil.getQueryValueByKey("currentPath"); 120 | 121 | if (!files) return null; 122 | 123 | return ( 124 |
125 |

126 | {currentPath ? currentPath[currentPath.length - 1] : ""} 127 |

128 |
129 |
130 | {files 131 | .toSorted((a, b) => { 132 | if (a.type === b.type) { 133 | return a.filename.localeCompare(b.filename); 134 | } 135 | if (a.type === "directory") { 136 | return -1; 137 | } 138 | return 1; 139 | }) 140 | .map((file) => ( 141 |
handleClickFile(file)} 144 | key={file.filename} 145 | > 146 |
149 | {getFileIcon(file.type)} 150 |
151 |
152 |

153 | {getFileNameByPath(file.filename)} 154 |

155 |
156 | 157 |
158 | ))} 159 |
160 |
161 |
162 | ); 163 | }; 164 | 165 | export default FileList; 166 | -------------------------------------------------------------------------------- /app/components/Reader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@mui/joy"; 4 | import React from "react"; 5 | import { AuthType, createClient } from "webdav"; 6 | 7 | function reader() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | 15 | export default reader; 16 | -------------------------------------------------------------------------------- /app/components/ServerTabs.tsx: -------------------------------------------------------------------------------- 1 | import { CircularProgress, TabList, TabPanel, Tabs } from "@mui/joy"; 2 | import React from "react"; 3 | import SeverTab from "./SeverTab"; 4 | 5 | import { IServerFormData } from "../interface"; 6 | 7 | import useQueryDirectoryContents from "../hook/query/useQueryDirectoryContents"; 8 | import _ from "lodash"; 9 | import FileList from "./FileList"; 10 | import useIndexStore from "../store"; 11 | 12 | import useSearchParamsUtil from "../hook/useSearchParamsUtil"; 13 | 14 | import useSyncPagination from "../hook/query/useSyncPagination"; 15 | import useQueryFileList from "../hook/query/useQueryFileList"; 16 | import useServerActions from "../hook/actions/useServerActions"; 17 | import useDeviceWidth from "../hook/useDeviceWidth"; 18 | function ServerTabs({ servers }: { servers: IServerFormData[] }) { 19 | const { updateCurrentServerAndPushToPath } = useServerActions(); 20 | const screenWidth = useDeviceWidth(); 21 | 22 | useSyncPagination(); 23 | const { fileList, isLoading } = useQueryFileList(); 24 | const { getQueryValueByKey } = useSearchParamsUtil(); 25 | const currentServer = getQueryValueByKey("currentServer"); 26 | const isDesktop = screenWidth >= 768; 27 | const propsByDevice: any = isDesktop 28 | ? { 29 | Tabs: { 30 | "aria-label": "Vertical tabs", 31 | orientation: "vertical", 32 | }, 33 | TabList: { 34 | sx: { 35 | width: "300px", 36 | height: "94vh", 37 | }, 38 | }, 39 | } 40 | : {}; 41 | return ( 42 | 46 | 47 | {servers.map((server) => { 48 | return ; 49 | })} 50 | 51 | 52 | 53 | {isLoading ? ( 54 | 55 | ) : ( 56 | 57 | )} 58 | 59 | 60 | ); 61 | } 62 | 63 | export default ServerTabs; 64 | -------------------------------------------------------------------------------- /app/components/ServerViewer.less: -------------------------------------------------------------------------------- 1 | .add { 2 | width: 120px; 3 | 4 | } -------------------------------------------------------------------------------- /app/components/ServerViewer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Add, Person, PlusOne } from "@mui/icons-material"; 3 | import { Box, Button, Sheet, Tab, TabList, TabPanel, Tabs } from "@mui/joy"; 4 | import React, { useEffect } from "react"; 5 | import useAddServerModal from "../store/useAddServerModal"; 6 | import useServerViewerStore from "../store/useServerViewerStore"; 7 | import SeverTab from "./SeverTab"; 8 | import ServerTabs from "./ServerTabs"; 9 | import useIndexStore from "../store"; 10 | 11 | function TabsForSelectServer() { 12 | const { open, setOpen } = useAddServerModal(); 13 | const { servers } = useServerViewerStore(); 14 | 15 | return ( 16 |
17 | 18 |
19 |
20 | 27 |
28 |
29 |
30 |
31 | 32 |
33 | ); 34 | } 35 | 36 | export default TabsForSelectServer; 37 | -------------------------------------------------------------------------------- /app/components/SeverTab.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IconButton, ListItemDecorator, Tab, Typography } from "@mui/joy"; 3 | import React from "react"; 4 | import { IServerFormData } from "../interface"; 5 | import { getSeverId } from "../utils"; 6 | import { Icon } from "@mui/material"; 7 | import { ComputerRounded, Delete, Edit } from "@mui/icons-material"; 8 | import useAddServerModal from "../store/useAddServerModal"; 9 | import AlertDialogModal from "../ui/AlertDialog"; 10 | import useServerActions from "../hook/actions/useServerActions"; 11 | 12 | function SeverTab({ server }: { server: IServerFormData }) { 13 | 14 | const { open, setOpen, setCurrentEditServer } = useAddServerModal(); 15 | const { removeServer, updateCurrentServerAndPushToPath } = useServerActions(); 16 | const handleOpenEditServerModal = () => { 17 | setOpen(true); 18 | setCurrentEditServer(server); 19 | }; 20 | const handleRemoveServer = (server: IServerFormData) => { 21 | 22 | removeServer(server.id); 23 | }; 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 |
{ 33 | updateCurrentServerAndPushToPath(server); 34 | }} 35 | > 36 | {getSeverId(server)} 37 |
38 | 39 | 40 | 41 | 42 | handleRemoveServer(server)} /> 43 |
44 | ); 45 | } 46 | 47 | export default SeverTab; 48 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmfb/repub/37cdb10a92cbb5ebbe9d0eb8f5badeea61486153/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | .add-server-button-container { 7 | @apply w-[300px] flex justify-center items-center; 8 | } 9 | .header-container { 10 | @apply h-[6vh] flex justify-between items-center; 11 | } 12 | } -------------------------------------------------------------------------------- /app/hook/actions/useServerActions.tsx: -------------------------------------------------------------------------------- 1 | import useIndexStore from "@/app/store"; 2 | import useSearchParamsUtil from "../useSearchParamsUtil"; 3 | import useServerViewerStore from "@/app/store/useServerViewerStore"; 4 | import { IServerFormData } from "@/app/interface"; 5 | import { getBasePathFromHost } from "@/app/utils"; 6 | 7 | function useServerActions() { 8 | const { setCurrentServer } = useIndexStore(); 9 | const { removeServerConfig } = useServerViewerStore(); 10 | const { createQueryStringFromObj, push } = useSearchParamsUtil(); 11 | 12 | const updateCurrentServerAndPushToPath = (server: IServerFormData) => { 13 | const [host, basePath] = getBasePathFromHost(server.host); 14 | setCurrentServer(server); 15 | const currentPath = [basePath]; 16 | const qs = createQueryStringFromObj({ 17 | currentServer: JSON.stringify(server), 18 | currentPath: JSON.stringify(currentPath), 19 | }); 20 | 21 | push(qs); 22 | }; 23 | 24 | const removeServer = (serverId: any) => { 25 | removeServerConfig(serverId); 26 | push(); 27 | }; 28 | return { 29 | updateCurrentServerAndPushToPath, 30 | removeServer, 31 | }; 32 | } 33 | export default useServerActions; 34 | -------------------------------------------------------------------------------- /app/hook/query/useQueryBook.ts: -------------------------------------------------------------------------------- 1 | import { getDirectoryContents, getFile } from "@/app/clientApi"; 2 | import { IFile } from "@/app/components/FileList"; 3 | import { IServerFormData } from "@/app/interface"; 4 | import useIndexStore from "@/app/store"; 5 | import useBooksContent from "@/app/store/useBooksContent"; 6 | import { getBookId } from "@/app/utils"; 7 | import { repubCache } from "@/app/utils/cache"; 8 | 9 | 10 | import { useMutation, useQuery } from "@tanstack/react-query"; 11 | import { useRouter } from "next/navigation"; 12 | import { v4 as uuidv4 } from "uuid"; 13 | 14 | function useQueryBook() { 15 | const { addBook } = useBooksContent(); 16 | const { addBook: addBookPersist } = useIndexStore(); 17 | const router = useRouter(); 18 | 19 | const query = useMutation({ 20 | mutationKey: ["useQueryBook"], 21 | mutationFn: ({ file, server }: { file: IFile; server: IServerFormData }) => 22 | getFile(file, server), 23 | onSuccess: async (res, variables) => { 24 | const bookId = getBookId(variables.file, variables.server); 25 | const cachedBookRes = await repubCache.read(bookId); 26 | 27 | if (!cachedBookRes) { 28 | await repubCache.create(bookId, new Response(res.data)); 29 | } 30 | const book = { 31 | id: bookId, 32 | name: variables.file.filename, 33 | 34 | serverId: variables.server.id as any, 35 | content: res.data, 36 | }; 37 | 38 | addBook(book); 39 | addBookPersist(book); 40 | router.push(`/viewer/${bookId}`); 41 | }, 42 | }); 43 | return query; 44 | } 45 | export default useQueryBook; 46 | -------------------------------------------------------------------------------- /app/hook/query/useQueryDirectoryContents.ts: -------------------------------------------------------------------------------- 1 | import { getDirectoryContents } from "@/app/clientApi"; 2 | import { IServerFormData } from "@/app/interface"; 3 | import useIndexStore from "@/app/store"; 4 | import { useMutation, useQuery } from "@tanstack/react-query"; 5 | import { useRouter } from "next/navigation"; 6 | import queryString from "query-string"; 7 | 8 | function useQueryDirectoryContents() { 9 | const { pushPath, setFileLists } = useIndexStore(); 10 | const router = useRouter(); 11 | const query = useMutation({ 12 | mutationKey: ["directoryContents"], 13 | mutationFn: ({ path, server }: { path: string; server: IServerFormData }) => 14 | getDirectoryContents(path, server), 15 | onSuccess: (res, variables) => { 16 | const prevQuery = queryString.parse(window.location.search); 17 | const { currentPath } = prevQuery; 18 | const cpPrev = currentPath ? JSON.parse(currentPath as string) : []; 19 | const cp = [...cpPrev, variables.path]; 20 | const qs = queryString.stringify({ 21 | currentServer: JSON.stringify(variables.server), 22 | currentPath: JSON.stringify(cp), 23 | }); 24 | 25 | router.push(`/?${qs}`); 26 | }, 27 | }); 28 | return query; 29 | } 30 | export default useQueryDirectoryContents; 31 | -------------------------------------------------------------------------------- /app/hook/query/useQueryFileList.ts: -------------------------------------------------------------------------------- 1 | import { getDirectoryContents } from "@/app/clientApi"; 2 | import { useQuery } from "@tanstack/react-query"; 3 | import useSearchParamsUtil from "../useSearchParamsUtil"; 4 | import _ from "lodash"; 5 | 6 | function useQueryFileList() { 7 | const { getQueryValueByKey } = useSearchParamsUtil(); 8 | const currentServer = getQueryValueByKey("currentServer"); 9 | const currentPath = getQueryValueByKey("currentPath"); 10 | const { data: fileListsRes, isLoading } = useQuery({ 11 | queryKey: ["fileLists", currentPath], 12 | queryFn: () => 13 | getDirectoryContents(currentPath[currentPath.length - 1], currentServer), 14 | enabled: !!currentPath && !_.isEmpty(currentServer), 15 | retry(failureCount, error) { 16 | if (failureCount >= 2) { 17 | return false; 18 | } 19 | return true; 20 | }, 21 | }); 22 | 23 | const fileList = _.get(fileListsRes, "data.data"); 24 | return { 25 | fileList, 26 | isLoading, 27 | }; 28 | } 29 | export default useQueryFileList; 30 | -------------------------------------------------------------------------------- /app/hook/query/useSyncPagination.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import useSearchParamsUtil from "../useSearchParamsUtil"; 3 | import { getDirectoryContents, getFile } from "@/app/clientApi"; 4 | import { useEffect } from "react"; 5 | import { blobToJson, getBasePathFromHost, getConfigPath } from "@/app/utils"; 6 | import useIndexStore from "@/app/store"; 7 | import _ from "lodash"; 8 | 9 | function useSyncPagination() { 10 | const { setCurrentServer, setBooks } = useIndexStore(); 11 | const { 12 | getQueryValueByKey, 13 | createQueryString, 14 | createQueryStringFromObj, 15 | push, 16 | } = useSearchParamsUtil(); 17 | const currentServer = getQueryValueByKey("currentServer"); 18 | const [_host, basePath] = getBasePathFromHost(currentServer?.host); 19 | const isCurrentServerExist = !_.isEmpty(currentServer); 20 | 21 | const { data: syncFileExistRes } = useQuery({ 22 | queryKey: ["storageExist", currentServer], 23 | queryFn: () => getDirectoryContents(basePath, currentServer), 24 | enabled: isCurrentServerExist, 25 | }); 26 | 27 | const hasSyncFile = () => { 28 | const data = _.get(syncFileExistRes, "data.data", []); 29 | const hasSyncFile = data.find((item: any) => item.basename === "repub"); 30 | const res = !!hasSyncFile; 31 | return res; 32 | }; 33 | 34 | 35 | 36 | const { data: booksPaginationRes, isLoading: isBPLoading } = useQuery({ 37 | queryKey: ["booksPagination", currentServer], 38 | queryFn: () => 39 | getFile( 40 | { 41 | filename: getConfigPath(currentServer, "index-storage.json"), 42 | } as any, 43 | currentServer 44 | ), 45 | enabled: isCurrentServerExist && hasSyncFile(), 46 | }); 47 | 48 | useEffect(() => { 49 | const getRes = async () => { 50 | if (!(booksPaginationRes?.data instanceof Blob)) { 51 | return; 52 | } 53 | const res: any = await blobToJson(booksPaginationRes?.data); 54 | 55 | if (!res) { 56 | return; 57 | } 58 | setBooks(res.books); 59 | }; 60 | getRes(); 61 | }, [booksPaginationRes]); 62 | } 63 | export default useSyncPagination; 64 | -------------------------------------------------------------------------------- /app/hook/useCachedBookContent.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { repubCache } from "../utils/cache"; 3 | 4 | function useCachedBookContent(key: string) { 5 | const [data, setData] = useState(null); 6 | 7 | useEffect(() => { 8 | const getCBook = async () => { 9 | const cachedBookRes: any = await repubCache.read(key); 10 | if (!cachedBookRes) { 11 | return () => {}; 12 | } 13 | const content = await cachedBookRes.blob(); 14 | setData({ 15 | content, 16 | }); 17 | }; 18 | getCBook(); 19 | }, [key]); 20 | 21 | return data; 22 | } 23 | export default useCachedBookContent; 24 | -------------------------------------------------------------------------------- /app/hook/useDeviceWidth.ts: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | 3 | const useDeviceWidth = () => { 4 | const [screenWidth, setScreenWidth] = useState(window.innerWidth); 5 | 6 | useEffect(() => { 7 | const handleResize = () => { 8 | setScreenWidth(window.innerWidth); 9 | }; 10 | window.addEventListener("resize", handleResize); 11 | 12 | return () => { 13 | window.removeEventListener("resize", handleResize); 14 | }; 15 | }, []); 16 | 17 | return screenWidth; 18 | }; 19 | 20 | export default useDeviceWidth; 21 | -------------------------------------------------------------------------------- /app/hook/useSearchParamsUtil.ts: -------------------------------------------------------------------------------- 1 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 2 | import { useCallback } from "react"; 3 | 4 | function useSearchParamsUtil() { 5 | const router = useRouter(); 6 | const pathname = usePathname(); 7 | const searchParams = useSearchParams(); 8 | 9 | // Get a new searchParams string by merging the current 10 | // searchParams with a provided key/value pair 11 | const createQueryString = useCallback( 12 | (name: string, value: string) => { 13 | const params = new URLSearchParams(searchParams); 14 | params.set(name, value); 15 | 16 | return params.toString(); 17 | }, 18 | [searchParams] 19 | ); 20 | 21 | const createQueryStringFromObj = useCallback( 22 | (obj: Record) => { 23 | const params = new URLSearchParams(searchParams); 24 | for (const key in obj) { 25 | if (obj.hasOwnProperty(key)) { 26 | const value = obj[key]; 27 | params.set(key, value); 28 | } 29 | } 30 | return params.toString(); 31 | }, 32 | [searchParams] // No dependencies 33 | ); 34 | 35 | const getQueryValueByKey = useCallback( 36 | (key: string) => { 37 | const currentPath = searchParams.get(key); 38 | if (!currentPath) return null; 39 | return JSON.parse(currentPath as any); 40 | }, 41 | [searchParams] 42 | ); 43 | 44 | const push = (qs?: string) => { 45 | if (!qs) { 46 | router.push(pathname); 47 | return; 48 | } 49 | router.push(pathname + "?" + qs); 50 | }; 51 | 52 | return { 53 | getQueryValueByKey, 54 | createQueryString, 55 | createQueryStringFromObj, 56 | push, 57 | }; 58 | } 59 | export default useSearchParamsUtil; 60 | -------------------------------------------------------------------------------- /app/interface/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthType } from "webdav"; 2 | import { ILocation } from "../viewer/[bookId]/types"; 3 | 4 | export interface IServerFormData { 5 | id?: string; 6 | authType: AuthType; 7 | protocol: "http" | "https"; 8 | host: string; 9 | port: number; 10 | username: string; 11 | password: string; 12 | } 13 | 14 | export interface IBook { 15 | id: string; 16 | name: string; 17 | location?: string; 18 | serverId: string; 19 | content: any; 20 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import "./globals.css"; 4 | import { ReactQueryProvider } from "./provider/react-query-provider"; 5 | 6 | export const metadata: Metadata = { 7 | title: "Repub", 8 | description: "a simple reader which can read your epub files online", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | return ( 17 | 18 | 19 | {children} 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { toast } from "react-toastify"; 3 | 4 | const axiosInstance = axios.create({ 5 | baseURL: "/api", 6 | }); 7 | 8 | axiosInstance.interceptors.response.use( 9 | (response) => { 10 | return response; 11 | }, 12 | (error) => { 13 | 14 | toast.error(error.message); 15 | return Promise.reject(error); 16 | } 17 | ); 18 | 19 | export default axiosInstance; 20 | -------------------------------------------------------------------------------- /app/lib/winston.ts: -------------------------------------------------------------------------------- 1 | const winston = require("winston"); 2 | 3 | const logger = winston.createLogger({ 4 | level: "info", 5 | format: winston.format.json(), 6 | defaultMeta: { service: "user-service" }, 7 | transports: [ 8 | // 9 | // - Write all logs with importance level of `error` or less to `error.log` 10 | // - Write all logs with importance level of `info` or less to `combined.log` 11 | // 12 | new winston.transports.File({ filename: "error.log", level: "error" }), 13 | new winston.transports.File({ filename: "combined.log" }), 14 | ], 15 | }); 16 | 17 | export const logError = ({ 18 | error, 19 | requestBody, 20 | location, 21 | }: { 22 | error: Error; 23 | requestBody: any; 24 | location: string; 25 | }) => { 26 | const strAll = JSON.stringify({ 27 | errorMessage: error.message, 28 | requestBody, 29 | location, 30 | }); 31 | logger.error(strAll); 32 | }; 33 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { AuthType, createClient } from "webdav"; 3 | import Reader from "./components/Reader"; 4 | import ConnectModal from "./components/ConnectModal"; 5 | import ServerViewer from "./components/ServerViewer"; 6 | import { ToastContainer, toast } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | import useIndexStore from "./store"; 9 | import { useEffect } from "react"; 10 | 11 | export default function Home() { 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/provider/react-query-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 5 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 6 | 7 | export const queryClient = new QueryClient(); 8 | 9 | export function ReactQueryProvider({ children }: React.PropsWithChildren) { 10 | const [client] = React.useState(queryClient); 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary modules 2 | import { SnackbarProps, SnackbarTypeMap } from "@mui/joy"; 3 | import { create } from "zustand"; 4 | import { immer } from "zustand/middleware/immer"; 5 | import { IBook, IServerFormData } from "../interface"; 6 | import { IFile } from "../components/FileList"; 7 | import { persist, createJSONStorage } from "zustand/middleware"; 8 | import { getClientConfigFromServer, getClientConfigFromUrl } from "../utils"; 9 | import queryString from "query-string"; 10 | import _ from "lodash"; 11 | import { createClient } from "webdav"; 12 | 13 | 14 | import axios from "../lib/axios"; 15 | interface IndexState { 16 | books: Array; 17 | currentPath: Array; 18 | currentServer: IServerFormData; 19 | fileLists: IFile[]; 20 | } 21 | 22 | interface IndexActions { 23 | addBook: (book: IBook) => void; 24 | setBooks: (books: IBook[]) => void; 25 | pushPath: (path: string) => void; 26 | popPath: () => void; 27 | setFileLists: (fileLists: IFile[]) => void; 28 | setCurrentServer: (server: IServerFormData) => void; 29 | } 30 | 31 | // Create your store 32 | const useIndexStore = create()( 33 | persist( 34 | immer((set) => ({ 35 | books: [], 36 | currentPath: [], 37 | currentServer: {} as any, 38 | fileLists: [], 39 | 40 | addBook: (book: IBook) => 41 | set((state) => { 42 | const hasBook = state.books.find((b) => b.id === book.id); 43 | if (hasBook) { 44 | return; 45 | } 46 | state.books.push(book); 47 | }), 48 | setBooks: (books: IBook[]) => 49 | set((state) => { 50 | state.books = books; 51 | }), 52 | pushPath: (path: string) => 53 | set((state) => { 54 | state.currentPath.push(path); 55 | }), 56 | popPath: () => 57 | set((state) => { 58 | state.currentPath.pop(); 59 | }), 60 | 61 | setFileLists: (fileLists: IFile[]) => 62 | set((state) => { 63 | state.fileLists = fileLists; 64 | }), 65 | setCurrentServer: (server: IServerFormData) => 66 | set((state) => { 67 | state.currentServer = server; 68 | }), 69 | })), 70 | { 71 | name: "index-storage", // name of the item in the storage (must be unique) 72 | partialize: (state) => { 73 | // const syncStateToWebdav = async () => { 74 | // const currentServer = state.currentServer; 75 | // if (_.isEmpty(currentServer)) { 76 | // return; 77 | // } 78 | // const isEmptyLocal = _.isEmpty(state.books); 79 | // if (isEmptyLocal) { 80 | // return; 81 | // } 82 | 83 | // const res = await axios.post( 84 | // `/webdav/putFileContents?${queryString.stringify(currentServer)}`, 85 | // _.pick(state, ["books", "currentServer"]) 86 | // ); 87 | // }; 88 | // syncStateToWebdav(); 89 | return state; 90 | }, 91 | } 92 | ) 93 | ); 94 | 95 | 96 | 97 | 98 | export default useIndexStore; 99 | -------------------------------------------------------------------------------- /app/store/progressStore.ts: -------------------------------------------------------------------------------- 1 | import { persist } from "zustand/middleware"; 2 | import { immer } from "zustand/middleware/immer"; 3 | import { createStore } from "zustand/vanilla"; 4 | 5 | const store = createStore( 6 | persist( 7 | immer((set) => ({ 8 | books: {}, 9 | setBooks: (bookId: string, value: any) => 10 | set((state: any) => { 11 | state.books[bookId] = value; 12 | }), 13 | })), 14 | { 15 | name: "download-progress-storage", 16 | } 17 | ) 18 | ); 19 | const { getState, setState, subscribe } = store; 20 | 21 | export default store; 22 | -------------------------------------------------------------------------------- /app/store/useAddServerModal.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary modules 2 | import { create } from "zustand"; 3 | import { immer } from "zustand/middleware/immer"; 4 | import { IServerFormData } from "../interface"; 5 | 6 | interface ModalState { 7 | open: boolean; 8 | currentEditServer: IServerFormData; 9 | } 10 | 11 | interface ModalActions { 12 | setOpen: (open: boolean) => void; 13 | setCurrentEditServer: (server: IServerFormData) => void; 14 | } 15 | 16 | // Create your store 17 | const useAddServerModal = create()( 18 | immer((set) => ({ 19 | open: false, 20 | currentEditServer: {} as any, 21 | setOpen: (open: boolean) => 22 | set((state) => { 23 | state.open = open; 24 | }), 25 | setCurrentEditServer: (server: IServerFormData) => 26 | set((state) => { 27 | state.currentEditServer = server; 28 | }), 29 | })) 30 | ); 31 | 32 | export default useAddServerModal; 33 | -------------------------------------------------------------------------------- /app/store/useBooksContent.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary modules 2 | import { SnackbarProps, SnackbarTypeMap } from "@mui/joy"; 3 | import { create } from "zustand"; 4 | import { immer } from "zustand/middleware/immer"; 5 | import { IBook } from "../interface"; 6 | import { IFile } from "../components/FileList"; 7 | import { persist, createJSONStorage } from "zustand/middleware"; 8 | interface IndexState { 9 | books: Array; 10 | } 11 | 12 | interface IndexActions { 13 | addBook: (book: IBook) => void; 14 | setBooks: (books: IBook[]) => void; 15 | } 16 | 17 | // Create your store 18 | const useBooksContent = create()( 19 | immer((set) => ({ 20 | books: [], 21 | addBook: (book: IBook) => 22 | set((state) => { 23 | const hasBook = state.books.find((b) => b.id === book.id); 24 | if (hasBook) { 25 | return; 26 | } 27 | state.books.push(book); 28 | }), 29 | setBooks: (books: IBook[]) => 30 | set((state) => { 31 | state.books = books; 32 | }), 33 | })) 34 | ); 35 | 36 | export default useBooksContent; 37 | -------------------------------------------------------------------------------- /app/store/useServerViewerStore.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary modules 2 | import { create } from "zustand"; 3 | import { immer } from "zustand/middleware/immer"; 4 | import { IServerFormData } from "../interface"; 5 | import { persist, createJSONStorage } from "zustand/middleware"; 6 | // Define your store states 7 | interface ServerState { 8 | servers: IServerFormData[]; 9 | } 10 | 11 | interface ServerActions { 12 | addServerConfig: (config: IServerFormData) => void; 13 | removeServerConfig: (id: string) => void; 14 | updateServerConfig: (config: IServerFormData, id: string) => void; 15 | } 16 | 17 | // Create your store 18 | const useServerViewerStore = create()( 19 | persist( 20 | immer((set) => ({ 21 | servers: [], 22 | 23 | addServerConfig: (config: IServerFormData) => 24 | set((state) => { 25 | state.servers.push(config); 26 | }), 27 | removeServerConfig: (id: string) => 28 | set((state) => { 29 | const index = state.servers.findIndex((s) => s.id === id); 30 | 31 | if (index === -1) { 32 | return; 33 | } 34 | state.servers.splice(index, 1); 35 | }), 36 | updateServerConfig: (config: IServerFormData, id: string) => 37 | set((state) => { 38 | const index = state.servers.findIndex((s) => s.id === id); 39 | if (index === -1) { 40 | return; 41 | } 42 | state.servers[index] = config; 43 | }), 44 | })), 45 | { 46 | name: "serverViewerStore", 47 | } 48 | ) 49 | ); 50 | 51 | export default useServerViewerStore; 52 | -------------------------------------------------------------------------------- /app/ui/AlertDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "@mui/joy/Button"; 3 | import Divider from "@mui/joy/Divider"; 4 | import DialogTitle from "@mui/joy/DialogTitle"; 5 | import DialogContent from "@mui/joy/DialogContent"; 6 | import DialogActions from "@mui/joy/DialogActions"; 7 | import Modal from "@mui/joy/Modal"; 8 | import ModalDialog from "@mui/joy/ModalDialog"; 9 | import DeleteForever from "@mui/icons-material/DeleteForever"; 10 | import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; 11 | import { IconButton } from "@mui/joy"; 12 | 13 | export default function AlertDialogModal(props: { onConfirm: () => void }) { 14 | const [open, setOpen] = React.useState(false); 15 | return ( 16 | 17 | setOpen(true)}> 18 | 19 | 20 | 21 | setOpen(false)}> 22 | 23 | 24 | 25 | 确认 26 | 27 | 28 | 确定要删除吗? 29 | 30 | 40 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/utils/api.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { logError } from "../lib/winston"; 3 | 4 | function withErrorHandler(fn: any) { 5 | return async function (request: NextRequest, response: NextResponse) { 6 | try { 7 | return await fn(request, response); 8 | } catch (error: any) { 9 | // Log the error to a logging system 10 | logError({ error, requestBody: request.json(), location: fn.name }); 11 | // Respond with a generic 500 Internal Server Error 12 | return new Response("Internal Server Error", { status: 500 }); 13 | } 14 | }; 15 | } 16 | 17 | function successResponse(data: any) { 18 | return NextResponse.json({ 19 | data, 20 | status: "success", 21 | ts: Date.now(), 22 | }); 23 | } 24 | 25 | export { withErrorHandler, successResponse }; 26 | -------------------------------------------------------------------------------- /app/utils/cache.ts: -------------------------------------------------------------------------------- 1 | class CacheUtil { 2 | cacheName: string; 3 | 4 | constructor(cacheName: string) { 5 | this.cacheName = cacheName; 6 | } 7 | 8 | async create(key: string, data: Response): Promise { 9 | const cache = await caches.open(this.cacheName); 10 | cache.put(key, data); 11 | } 12 | 13 | async read(key: string): Promise { 14 | const cache = await caches.open(this.cacheName); 15 | return cache.match(`/${key}`); 16 | } 17 | 18 | async update(key: string, data: Response): Promise { 19 | return this.create(key, data); 20 | } 21 | 22 | async delete(key: string): Promise { 23 | const cache = await caches.open(this.cacheName); 24 | cache.delete(key); 25 | } 26 | 27 | async keys(): Promise { 28 | const cache = await caches.open(this.cacheName); 29 | const keys = await cache.keys(); 30 | return keys; 31 | } 32 | } 33 | 34 | export const repubCache = new CacheUtil("repub-book"); 35 | -------------------------------------------------------------------------------- /app/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { IServerQueryObj } from "../api/webdav/route"; 2 | import { IFile } from "../components/FileList"; 3 | import { IServerFormData } from "../interface"; 4 | import queryString from "query-string"; 5 | import { WebDAVClient } from "webdav"; 6 | const md5 = require("md5"); 7 | export const isHttps = (protocol: string) => { 8 | return protocol === "https"; 9 | }; 10 | export const getBasePathFromHost = (host: string) => { 11 | if (!host) { 12 | return ["", ""]; 13 | } 14 | const isIncludeSlash = host.includes("/"); 15 | 16 | if (isIncludeSlash) { 17 | const [url, basePath] = host.split("/"); 18 | return [url, `/${basePath}`]; 19 | } 20 | 21 | return [host, "/"]; 22 | }; 23 | 24 | export const getConfigPath = (server: IServerFormData, fileName?: string) => { 25 | const [_host, basePath] = getBasePathFromHost(server.host); 26 | const bPath = basePath === "/" ? "/repub" : `${basePath}/repub`; 27 | const configPath = fileName ? `${bPath}/${fileName}` : bPath; 28 | return configPath; 29 | }; 30 | export const getSeverId = (server: IServerFormData) => { 31 | if (!server) { 32 | return ""; 33 | } 34 | const [host] = getBasePathFromHost(server.host); 35 | 36 | return `${server.protocol}://${host}:${server.port}`; 37 | }; 38 | export const getClientConfigFromUrl: (url: string) => IServerQueryObj = ( 39 | url: string 40 | ) => { 41 | const parsed = queryString.parseUrl(url); 42 | const { query } = parsed; 43 | return { 44 | url: getSeverId(query as any), 45 | username: query.username as string, 46 | password: query.password as string, 47 | authType: query.authType as any, 48 | path: query.path as string, 49 | host: query.host as string, 50 | }; 51 | }; 52 | 53 | export const getClientConfigFromServer = ( 54 | server: IServerFormData, 55 | path: string = "/" 56 | ) => { 57 | return { 58 | url: getSeverId(server), 59 | username: server.username, 60 | password: server.password, 61 | authType: server.authType, 62 | path: path, 63 | }; 64 | }; 65 | 66 | export const getBookId = (file: IFile, server: IServerFormData) => { 67 | const id = `${getSeverId(server)}${file.filename}${file.lastmod}`; 68 | return md5(id); 69 | }; 70 | 71 | export const getFileNameByPath = (path: string) => { 72 | const arr = path.split("/"); 73 | return arr[arr.length - 1]; 74 | }; 75 | 76 | 77 | 78 | export const blobToJson = (blob: any) => { 79 | return new Promise((resolve, reject) => { 80 | const reader: any = new FileReader(); 81 | reader.onload = () => { 82 | resolve(JSON.parse(reader.result)); 83 | }; 84 | reader.onerror = reject; 85 | reader.readAsText(blob); 86 | }); 87 | }; -------------------------------------------------------------------------------- /app/utils/searchParams.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmfb/repub/37cdb10a92cbb5ebbe9d0eb8f5badeea61486153/app/utils/searchParams.ts -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/Reader/hooks/useRendition.ts: -------------------------------------------------------------------------------- 1 | import { IBook } from "@/app/interface"; 2 | import ePub from "epubjs"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import useReader from "../../../store"; 5 | import { ILocation } from "../../../types"; 6 | 7 | function useRendition({ 8 | book, 9 | nodeId, 10 | location, 11 | }: { 12 | book: IBook; 13 | nodeId: string; 14 | location: ILocation; 15 | }) { 16 | const displayed = useRef(false); 17 | const { rendition, setRendition } = useReader(); 18 | useEffect(() => { 19 | if (displayed.current || !book?.content) { 20 | return () => {}; 21 | } 22 | const render = async () => { 23 | var eBook = ePub(book.content); 24 | var rendition = eBook.renderTo(nodeId, { 25 | flow: "paginated", 26 | allowScriptedContent: true, 27 | }); 28 | 29 | const r: any = await rendition.display( 30 | location ? location.start : undefined 31 | ); 32 | 33 | setRendition(rendition); 34 | }; 35 | 36 | render(); 37 | 38 | displayed.current = true; 39 | }, [location, book]); 40 | return rendition; 41 | } 42 | export default useRendition; 43 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/Reader/index.tsx: -------------------------------------------------------------------------------- 1 | import { IBook } from "@/app/interface"; 2 | import ePub from "epubjs"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { useIsMounted } from "usehooks-ts"; 5 | import useRendition from "./hooks/useRendition"; 6 | import { useParams } from "next/navigation"; 7 | import useBooksContent from "@/app/store/useBooksContent"; 8 | import useIndexStore from "@/app/store"; 9 | import { repubCache } from "@/app/utils/cache"; 10 | import useCachedBookContent from "@/app/hook/useCachedBookContent"; 11 | import { EpubViewStyle, ReactReader, ReactReaderStyle } from "react-reader"; 12 | import useHookLocationChanged from "../ReaderContainer/hooks/useHookLocationChanged"; 13 | import useReader from "../../store"; 14 | import _ from "lodash"; 15 | 16 | function Reader() { 17 | const params = useParams(); 18 | const { books } = useBooksContent(); 19 | const { books: booksPagination } = useIndexStore(); 20 | const memoryBook: any = books.find((book) => book.id === params.bookId); 21 | const pBook: any = booksPagination.find((book) => book.id === params.bookId); 22 | const cachedBook = useCachedBookContent(params.bookId as any); 23 | const locationChanged = useHookLocationChanged(); 24 | const { setRendition } = useReader(); 25 | 26 | // const rendition = useRendition({ 27 | // book: cachedBook ? cachedBook : memoryBook, 28 | // nodeId: "area", 29 | // location: pBook?.location, 30 | // }); 31 | 32 | const book = cachedBook ? cachedBook : memoryBook; 33 | const content = book?.content; 34 | 35 | return ( 36 |
37 | locationChanged(epubcfi as any)} 41 | getRendition={(r: any) => { 42 | setRendition(r); 43 | }} 44 | epubViewStyles={{ 45 | ...EpubViewStyle, 46 | viewHolder: { 47 | ...EpubViewStyle.viewHolder, 48 | overflowY: "hidden", 49 | }, 50 | }} 51 | readerStyles={{ 52 | ..._.omit(ReactReaderStyle, ["reader"]), 53 | arrow: { 54 | display: "none", 55 | }, 56 | reader: { 57 | position: "absolute", 58 | top: 50, 59 | left: 20, 60 | bottom: 30, 61 | right: 20, 62 | }, 63 | }} 64 | epubOptions={{ 65 | allowScriptedContent: true, 66 | }} 67 | /> 68 |
69 | ); 70 | } 71 | export default Reader; 72 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/ReaderContainer/hooks/useHookClick.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import useReader from "../../../store"; 3 | 4 | function useHookClick() { 5 | const { rendition } = useReader(); 6 | useEffect(() => { 7 | if (!rendition) return; 8 | const handleClick = (event: any) => { 9 | const selection = event.view.document.getSelection(); 10 | const str = selection.toString(); 11 | if (selection && str) { 12 | // selection exists and has content 13 | return; 14 | } 15 | const width = event.view.document.body.clientWidth; 16 | const offsetX = event.clientX % width; 17 | 18 | const half = width / 2; 19 | 20 | if (offsetX < half) { 21 | rendition.prev(); 22 | } else { 23 | rendition.next(); 24 | } 25 | }; 26 | 27 | rendition.on("click", handleClick); 28 | 29 | return () => { 30 | rendition.off("click", handleClick); 31 | }; 32 | }, [rendition]); 33 | } 34 | export default useHookClick; 35 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/ReaderContainer/hooks/useHookKeyPress.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | import useReader from "../../../store"; 3 | 4 | function useHookKeyPress() { 5 | const { rendition } = useReader(); 6 | const movePage = useCallback( 7 | (type: "PREV" | "NEXT") => { 8 | if (!rendition) return; 9 | if (type === "PREV") rendition.prev(); 10 | else rendition.next(); 11 | }, 12 | [rendition] 13 | ); 14 | const handleKeyPress = useCallback( 15 | ({ key }: any) => { 16 | key && key === "ArrowLeft" && movePage("PREV"); 17 | key && key === "ArrowRight" && movePage("NEXT"); 18 | }, 19 | [movePage] 20 | ); 21 | 22 | useEffect(() => { 23 | if (!rendition) return; 24 | 25 | document.addEventListener("keyup", handleKeyPress, false); 26 | 27 | rendition.on("keyup", handleKeyPress); 28 | 29 | return () => { 30 | document.removeEventListener("keyup", handleKeyPress, false); 31 | rendition.off("keyup", handleKeyPress); 32 | }; 33 | }, [rendition, handleKeyPress]); 34 | } 35 | export default useHookKeyPress; 36 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/ReaderContainer/hooks/useHookLocationChanged.ts: -------------------------------------------------------------------------------- 1 | import useIndexStore from "@/app/store"; 2 | import { useParams } from "next/navigation"; 3 | import useReader from "../../../store"; 4 | import { useCallback, useEffect } from "react"; 5 | import { ILocation } from "../../../types"; 6 | import { syncStateToWebdav } from "@/app/clientApi"; 7 | 8 | function useHookLocationChanged() { 9 | const { books, setBooks, currentServer } = useIndexStore(); 10 | 11 | const params = useParams(); 12 | 13 | const locationChanged = (location: string) => { 14 | const booksWithLocation = books.map((book) => { 15 | if (book.id === params.bookId) { 16 | return { 17 | ...book, 18 | location, 19 | }; 20 | } 21 | return book; 22 | }); 23 | setBooks(booksWithLocation); 24 | syncStateToWebdav(currentServer, booksWithLocation); 25 | }; 26 | 27 | return locationChanged; 28 | } 29 | export default useHookLocationChanged; 30 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/components/ReaderContainer/index.tsx: -------------------------------------------------------------------------------- 1 | import useBooksContent from "@/app/store/useBooksContent"; 2 | import Reader from "../Reader"; 3 | import useIndexStore from "@/app/store"; 4 | import { useParams } from "next/navigation"; 5 | import useRendition from "../Reader/hooks/useRendition"; 6 | import useReader from "../../store"; 7 | import { useCallback, useEffect } from "react"; 8 | import useHookKeyPress from "./hooks/useHookKeyPress"; 9 | import { ILocation } from "../../types"; 10 | import useHookLocationChanged from "./hooks/useHookLocationChanged"; 11 | import useHookClick from "./hooks/useHookClick"; 12 | 13 | function ReaderContainer() { 14 | // useHookKeyPress(); 15 | // useHookLocationChanged(); 16 | useHookClick(); 17 | return ( 18 |
19 | 20 |
21 | ); 22 | } 23 | export default ReaderContainer; 24 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | 4 | import "regenerator-runtime"; 5 | import ReaderContainer from "./components/ReaderContainer"; 6 | 7 | function Page({ params }: { params: { bookId: string } }) { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | 15 | 16 | export default Page; 17 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/store/index.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary modules 2 | import { SnackbarProps, SnackbarTypeMap } from "@mui/joy"; 3 | import { create } from "zustand"; 4 | import { immer } from "zustand/middleware/immer"; 5 | 6 | import { persist, createJSONStorage } from "zustand/middleware"; 7 | import Rendition from "epubjs/types/rendition"; 8 | interface IndexState { 9 | rendition: Rendition; 10 | } 11 | 12 | interface IndexActions { 13 | setRendition: (rendition: Rendition) => void; 14 | } 15 | 16 | // Create your store 17 | const useReader = create()( 18 | immer((set) => ({ 19 | rendition: null as any, 20 | setRendition: (rendition: Rendition) => 21 | set((state) => { 22 | state.rendition = rendition; 23 | }), 24 | })) 25 | ); 26 | 27 | export default useReader; 28 | -------------------------------------------------------------------------------- /app/viewer/[bookId]/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ILocation { 2 | index: number; 3 | href: string; 4 | start: string; 5 | end: string; 6 | percentage: number; 7 | } 8 | -------------------------------------------------------------------------------- /combined.log: -------------------------------------------------------------------------------- 1 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 2 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 3 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 4 | {"level":"error","message":"{\"errorMessage\":\"buffer.toBlob is not a function\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 5 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 6 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 7 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 8 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 9 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 10 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 11 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 12 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 13 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 14 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 15 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 16 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 17 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 18 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 19 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 20 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 21 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 22 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 23 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 24 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 25 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 26 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 27 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 28 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 29 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 30 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 31 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 32 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 33 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 34 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 35 | {"level":"error","message":"{\"errorMessage\":\"request to https://dav.jianguoyun.com:80/ failed, reason: write EPROTO 404185E201000000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:355:\\n\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 36 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 37 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 38 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 39 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 40 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 41 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 42 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 43 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 44 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 45 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 46 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 47 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 48 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 49 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 50 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 51 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 52 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 53 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 54 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 409 Conflict\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 55 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 56 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 57 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 58 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 59 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 60 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 61 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 62 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 63 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 64 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 65 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 66 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 67 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 68 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 69 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 70 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 71 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 72 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 73 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 74 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 75 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 76 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 77 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 78 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 79 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 80 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 81 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 82 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 83 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 84 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 85 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 86 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 87 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 88 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 89 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 90 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 91 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 92 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 93 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 94 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 95 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 96 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 97 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 98 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 99 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 100 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 101 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 102 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 103 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 104 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 105 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 106 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 107 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 108 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 109 | -------------------------------------------------------------------------------- /error.log: -------------------------------------------------------------------------------- 1 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 2 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 3 | {"level":"error","message":"{\"errorMessage\":\"Unexpected end of JSON input\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 4 | {"level":"error","message":"{\"errorMessage\":\"buffer.toBlob is not a function\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 5 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 6 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 7 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 8 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 9 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 10 | {"level":"error","message":"{\"errorMessage\":\"request to http://www.stardusted.top:31580/ failed, reason: read ECONNRESET\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 11 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 12 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 13 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 14 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 15 | {"level":"error","message":"{\"errorMessage\":\"request to http://localhost:31080/ failed, reason: connect ECONNREFUSED ::1:31080\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 16 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 17 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 18 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 19 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 20 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 21 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 22 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 23 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 24 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 25 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 26 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 27 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 28 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 29 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 30 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 31 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 32 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 33 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 403 Forbidden\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 34 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 35 | {"level":"error","message":"{\"errorMessage\":\"request to https://dav.jianguoyun.com:80/ failed, reason: write EPROTO 404185E201000000:error:0A00010B:SSL routines:ssl3_get_record:wrong version number:../deps/openssl/openssl/ssl/record/ssl3_record.c:355:\\n\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 36 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 37 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 38 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 39 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 401 Unauthorized\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 40 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 41 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 42 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 43 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 44 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 45 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 46 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 47 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 48 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 49 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 50 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 51 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 52 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 53 | {"level":"error","message":"{\"errorMessage\":\"Invalid URL\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 54 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 409 Conflict\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 55 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 56 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 57 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 58 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 59 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 404 Not Found\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 60 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 61 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 62 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 63 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 64 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 65 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 66 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 67 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 68 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 69 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 70 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 71 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 72 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 73 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 74 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 75 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 76 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 77 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 78 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 79 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 80 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 81 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 82 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 83 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 84 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 85 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 86 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 87 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 88 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 89 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 90 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 91 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 92 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 93 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 94 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 95 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 96 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 97 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 98 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 99 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 100 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 101 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 102 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 103 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 104 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 105 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 106 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 400 Bad Request\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 107 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 108 | {"level":"error","message":"{\"errorMessage\":\"Invalid response: 410 Gone\",\"requestBody\":{},\"location\":\"\"}","service":"user-service"} 109 | -------------------------------------------------------------------------------- /file.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmfb/repub/37cdb10a92cbb5ebbe9d0eb8f5badeea61486153/file.epub -------------------------------------------------------------------------------- /file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmfb/repub/37cdb10a92cbb5ebbe9d0eb8f5badeea61486153/file.txt -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repub", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.1", 13 | "@emotion/styled": "^11.11.0", 14 | "@mui/icons-material": "^5.14.16", 15 | "@mui/joy": "^5.0.0-beta.14", 16 | "@mui/material": "^5.14.18", 17 | "@reduxjs/toolkit": "^1.5.1", 18 | "@tanstack/react-query": "^5.8.4", 19 | "@tanstack/react-query-devtools": "^5.8.4", 20 | "@types/uuid": "^9.0.7", 21 | "@vercel/analytics": "^1.1.1", 22 | "axios": "^1.6.2", 23 | "blob-stream": "^0.1.3", 24 | "epubjs": "^0.3.93", 25 | "immer": "^10.0.3", 26 | "jszip": "^3.10.1", 27 | "lodash": "^4.17.21", 28 | "md5": "^2.3.0", 29 | "next": "14.0.1", 30 | "query-string": "^8.1.0", 31 | "react": "^18", 32 | "react-dom": "^18", 33 | "react-hook-form": "^7.48.2", 34 | "react-reader": "^2.0.4", 35 | "react-redux": "^7.2.3", 36 | "react-toastify": "^9.1.3", 37 | "styled-components": "^5.2.3", 38 | "usehooks-ts": "^2.9.1", 39 | "uuid": "^9.0.1", 40 | "webdav": "^5.3.0", 41 | "winston": "^3.11.0", 42 | "zustand": "^4.4.6" 43 | }, 44 | "devDependencies": { 45 | "@types/blob-stream": "^0.1.33", 46 | "@types/lodash": "^4.14.202", 47 | "@types/node": "^20", 48 | "@types/react": "^18", 49 | "@types/react-dom": "^18", 50 | "@types/react-redux": "^7.1.16", 51 | "@types/styled-components": "^5.1.7", 52 | "autoprefixer": "^10.0.1", 53 | "eslint": "^8", 54 | "eslint-config-next": "14.0.1", 55 | "postcss": "^8", 56 | "tailwindcss": "^3.3.0", 57 | "typescript": "^5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------