diff --git a/components/CustomEmbedLinkMenu.tsx b/components/CustomEmbedLinkMenu.tsx new file mode 100644 index 0000000..f8e7742 --- /dev/null +++ b/components/CustomEmbedLinkMenu.tsx @@ -0,0 +1,109 @@ +import { Dispatch, Fragment, SetStateAction, useState } from 'react' +import toast from 'react-hot-toast' +import { useTranslation } from 'next-i18next' +import { Dialog, Transition } from '@headlessui/react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useClipboard } from 'use-clipboard-copy' + +import { getBaseUrl } from '../utils/getBaseUrl' + +export default function CustomEmbedLinkMenu({ + path, + menuOpen, + setMenuOpen, +}: { + path: string + menuOpen: boolean + setMenuOpen: Dispatch> +}) { + const { t } = useTranslation() + const clipboard = useClipboard() + const closeMenu = () => setMenuOpen(false) + + const filename = path.substring(path.lastIndexOf('/') + 1) + const [name, setName] = useState(filename) + + return ( + + +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+ + {t('Customise direct link')} + + + {t('Change the raw file direct link to a URL ending with the extension of the file.')}{' '} + + {t('What is this?')} + + + +
+

{t('Filename')}

+ setName(e.target.value)} + /> +

{t('Default')}

+
+ {`${getBaseUrl()}/api?path=${path}&raw=true`} +
+

{t('Customised')}

+
+ {`${getBaseUrl()}/api/name/`} + {name} + {`?path=${path}&raw=true`} +
+
+ +
+ +
+
+
+
+
+
+ ) +} diff --git a/components/DownloadBtnGtoup.tsx b/components/DownloadBtnGtoup.tsx index fdee1a8..c03910c 100644 --- a/components/DownloadBtnGtoup.tsx +++ b/components/DownloadBtnGtoup.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler } from 'react' +import { MouseEventHandler, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { IconProp } from '@fortawesome/fontawesome-svg-core' import toast from 'react-hot-toast' @@ -10,6 +10,7 @@ import { useRouter } from 'next/router' import { getBaseUrl } from '../utils/getBaseUrl' import { getReadablePath } from '../utils/getReadablePath' +import CustomEmbedLinkMenu from './CustomEmbedLinkMenu' const btnStyleMap = (btnColor?: string) => { const colorMap = { @@ -64,36 +65,46 @@ export const DownloadButton = ({ const DownloadButtonGroup: React.FC<{ downloadUrl: string }> = ({ downloadUrl }) => { const { asPath } = useRouter() const clipboard = useClipboard() + const [menuOpen, setMenuOpen] = useState(false) const { t } = useTranslation() return ( -
- window.open(downloadUrl)} - btnColor="blue" - btnText={t('Download')} - btnIcon="file-download" - btnTitle={t('Download the file directly through OneDrive')} - /> - {/* + +
+ window.open(downloadUrl)} + btnColor="blue" + btnText={t('Download')} + btnIcon="file-download" + btnTitle={t('Download the file directly through OneDrive')} + /> + {/* window.open(`/api/proxy?url=${encodeURIComponent(downloadUrl)}`)} btnColor="teal" btnText={t('Proxy download')} btnIcon="download" btnTitle={t('Download the file with the stream proxied through Vercel Serverless')} /> */} - { - clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`) - toast.success(t('Copied direct link to clipboard.')) - }} - btnColor="pink" - btnText={t('Copy direct link')} - btnIcon="copy" - btnTitle={t('Copy the permalink to the file to the clipboard')} - /> -
+ { + clipboard.copy(`${getBaseUrl()}/api?path=${getReadablePath(asPath)}&raw=true`) + toast.success(t('Copied direct link to clipboard.')) + }} + btnColor="pink" + btnText={t('Copy direct link')} + btnIcon="copy" + btnTitle={t('Copy the permalink to the file to the clipboard')} + /> + setMenuOpen(true)} + btnColor="teal" + btnText={t('Customise link')} + btnIcon="pen" + /> +
+ ) } diff --git a/components/SwitchLang.tsx b/components/SwitchLang.tsx index 8d40dd1..2f88daa 100644 --- a/components/SwitchLang.tsx +++ b/components/SwitchLang.tsx @@ -4,6 +4,7 @@ import { Menu, Transition } from '@headlessui/react' import { useRouter } from 'next/router' import Link from 'next/link' +import { useCookies, withCookies } from 'react-cookie' // https://headlessui.dev/react/menu#integrating-with-next-js const CustomLink = ({ href, children, as, locale, ...props }): JSX.Element => { @@ -28,6 +29,8 @@ const localeText = (locale: string): string => { const SwitchLang = () => { const { locales, pathname, query, asPath } = useRouter() + const [_, setCookie] = useCookies(['NEXT_LOCALE']) + return (
@@ -48,7 +51,13 @@ const SwitchLang = () => { {locales!.map(locale => ( - + setCookie('NEXT_LOCALE', locale, { path: '/' })} + >
{localeText(locale)}
@@ -62,4 +71,4 @@ const SwitchLang = () => { ) } -export default SwitchLang +export default withCookies(SwitchLang) diff --git a/components/previews/VideoPreview.tsx b/components/previews/VideoPreview.tsx index 4b775ad..8c01c88 100644 --- a/components/previews/VideoPreview.tsx +++ b/components/previews/VideoPreview.tsx @@ -1,4 +1,5 @@ import type { OdFileObject } from '../../types' +import { useState } from 'react' import { useRouter } from 'next/router' import { useClipboard } from 'use-clipboard-copy' import DPlayer from 'react-dplayer' @@ -13,11 +14,13 @@ import { DownloadButton } from '../DownloadBtnGtoup' import { DownloadBtnContainer, PreviewContainer } from './Containers' import FourOhFour from '../FourOhFour' import Loading from '../Loading' +import CustomEmbedLinkMenu from '../CustomEmbedLinkMenu' const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => { const { asPath } = useRouter() const clipboard = useClipboard() + const [menuOpen, setMenuOpen] = useState(false) const { t } = useTranslation() // OneDrive generates thumbnails for its video files, we pick the thumbnail with the highest resolution @@ -30,15 +33,16 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => { const { loading, error, - result: flvjs, + result: mpegts, } = useAsync(async () => { if (isFlv) { - return (await import('flv.js')).default + return (await import('mpegts.js')).default } }, [isFlv]) return ( <> + {error ? ( @@ -56,7 +60,7 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => { type: isFlv ? 'customFlv' : 'auto', customType: { customFlv: (video: HTMLVideoElement) => { - const flvPlayer = flvjs!.createPlayer({ + const flvPlayer = mpegts!.createPlayer({ type: 'flv', url: video.src, }) @@ -96,6 +100,12 @@ const VideoPreview: React.FC<{ file: OdFileObject }> = ({ file }) => { btnText={t('Copy direct link')} btnIcon="copy" /> + setMenuOpen(true)} + btnColor="teal" + btnText={t('Customise link')} + btnIcon="pen" + /> window.open(`iina://weblink?url=${file['@microsoft.graph.downloadUrl']}`)} diff --git a/package.json b/package.json index 070b44f..6182151 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,12 @@ "axios": "^0.25.0", "cors": "^2.8.5", "crypto-js": "^4.1.1", + "csstype": "^2.6.2", "dayjs": "^1.10.7", "emoji-regex": "^10.0.0", - "flv.js": "^1.6.2", "ioredis": "^4.28.2", "jszip": "^3.7.1", + "mpegts.js": "^1.6.10", "next": "^12.0.10", "next-i18next": "^10.2.0", "nextjs-progressbar": "^0.0.13", @@ -34,6 +35,7 @@ "react": "^17.0.2", "react-async-hook": "^4.0.0", "react-audio-player": "^0.17.0", + "react-cookie": "^4.1.1", "react-copy-to-clipboard": "^5.0.3", "react-dom": "^17.0.2", "react-dplayer": "^0.4.2", diff --git a/pages/_app.tsx b/pages/_app.tsx index 10c2c93..359db7b 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -25,6 +25,7 @@ import { } from '@fortawesome/free-regular-svg-icons' import { faSearch, + faPen, faCheck, faPlus, faMinus, @@ -109,6 +110,7 @@ library.add( faThLarge, faThList, faLanguage, + faPen, ...iconList ) diff --git a/pages/api/index.ts b/pages/api/index.ts index 752bec5..e0090ef 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -160,6 +160,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // Fetch password from remote file content if (authTokenPath !== '') { + // Don't server cached response for password protected folders + res.setHeader('Cache-Control', 'no-cache') + try { const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, { headers: { Authorization: `Bearer ${accessToken}` }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9893d29..974fe60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,15 +22,16 @@ specifiers: axios: ^0.25.0 cors: ^2.8.5 crypto-js: ^4.1.1 + csstype: ^2.6.2 dayjs: ^1.10.7 emoji-regex: ^10.0.0 eslint: 8.8.0 eslint-config-next: 12.0.10 eslint-config-prettier: ^8.3.0 - flv.js: ^1.6.2 i18next-parser: ^5.4.0 ioredis: ^4.28.2 jszip: ^3.7.1 + mpegts.js: ^1.6.10 next: ^12.0.10 next-i18next: ^10.2.0 nextjs-progressbar: ^0.0.13 @@ -41,6 +42,7 @@ specifiers: react: ^17.0.2 react-async-hook: ^4.0.0 react-audio-player: ^0.17.0 + react-cookie: ^4.1.1 react-copy-to-clipboard: ^5.0.3 react-dom: ^17.0.2 react-dplayer: ^0.4.2 @@ -72,11 +74,12 @@ dependencies: axios: 0.25.0 cors: 2.8.5 crypto-js: 4.1.1 + csstype: 2.6.19 dayjs: 1.10.7 emoji-regex: 10.0.0 - flv.js: 1.6.2 ioredis: 4.28.3 jszip: 3.7.1 + mpegts.js: 1.6.10 next: 12.0.10_react-dom@17.0.2+react@17.0.2 next-i18next: 10.2.0_61390be992b634a688f7c2555547b55b nextjs-progressbar: 0.0.13_next@12.0.10+react@17.0.2 @@ -84,10 +87,11 @@ dependencies: react: 17.0.2 react-async-hook: 4.0.0_react@17.0.2 react-audio-player: 0.17.0_react-dom@17.0.2+react@17.0.2 + react-cookie: 4.1.1_react@17.0.2 react-copy-to-clipboard: 5.0.4_react@17.0.2 react-dom: 17.0.2_react@17.0.2 react-dplayer: 0.4.2_react@17.0.2 - react-hot-toast: 2.2.0_react-dom@17.0.2+react@17.0.2 + react-hot-toast: 2.2.0_6bba596ee6fb84e656d75c53902ecf01 react-hotkeys-hook: 3.4.4_react-dom@17.0.2+react@17.0.2 react-markdown: 8.0.0_b08e3c15324cbe90a6ff8fcd416c932c react-reader: 0.20.5_react@17.0.2 @@ -393,6 +397,10 @@ packages: tailwindcss: 3.0.18_833e1018ad0d7954aa80c53675939269 dev: false + /@types/cookie/0.3.3: + resolution: {integrity: sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==} + dev: false + /@types/cors/2.8.12: resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} dev: true @@ -1066,6 +1074,11 @@ packages: safe-buffer: 5.1.2 dev: true + /cookie/0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + /copy-to-clipboard/3.3.1: resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==} dependencies: @@ -1138,6 +1151,10 @@ packages: hasBin: true dev: true + /csstype/2.6.19: + resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==} + dev: false + /csstype/3.0.10: resolution: {integrity: sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==} @@ -1783,13 +1800,6 @@ packages: readable-stream: 2.3.7 dev: true - /flv.js/1.6.2: - resolution: {integrity: sha512-xre4gUbX1MPtgQRKj2pxJENp/RnaHaxYvy3YToVVCrSmAWUu85b9mug6pTXF6zakUjNP2lFWZ1rkSX7gxhB/2A==} - dependencies: - es6-promise: 4.2.8 - webworkify-webpack: 2.1.5 - dev: false - /follow-redirects/1.14.7: resolution: {integrity: sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==} engines: {node: '>=4.0'} @@ -1977,10 +1987,12 @@ packages: slash: 3.0.0 dev: true - /goober/2.1.7: + /goober/2.1.7_csstype@2.6.19: resolution: {integrity: sha512-aCR8u3A/tTgSrZAHfJObhYC0xgdKoYm4GvE/UFmxmzgvj3TSF+3oFYWtmJ459WBewjOIoEsoOG81sDs1rn+W5w==} peerDependencies: csstype: ^2.6.2 + dependencies: + csstype: 2.6.19 dev: false /graceful-fs/4.2.9: @@ -3102,6 +3114,13 @@ packages: engines: {node: '>0.9'} dev: true + /mpegts.js/1.6.10: + resolution: {integrity: sha512-ZgX4b93cWk+EazOFRV4lekLqmc4rV7P+WMisG8N0F2M4/EiluPMNNWjuaurQfitak++AIc/ZVQ3IgM3cBcH7WA==} + dependencies: + es6-promise: 4.2.8 + webworkify-webpack: 2.1.5 + dev: false + /mri/1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -3637,6 +3656,17 @@ packages: react-dom: 17.0.2_react@17.0.2 dev: false + /react-cookie/4.1.1_react@17.0.2: + resolution: {integrity: sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==} + peerDependencies: + react: '>= 16.3.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + hoist-non-react-statics: 3.3.2 + react: 17.0.2 + universal-cookie: 4.0.4 + dev: false + /react-copy-to-clipboard/5.0.4_react@17.0.2: resolution: {integrity: sha512-IeVAiNVKjSPeGax/Gmkqfa/+PuMTBhutEvFUaMQLwE2tS0EXrAdgOpWDX26bWTXF3HrioorR7lr08NqeYUWQCQ==} peerDependencies: @@ -3670,14 +3700,14 @@ packages: react: 17.0.2 dev: false - /react-hot-toast/2.2.0_react-dom@17.0.2+react@17.0.2: + /react-hot-toast/2.2.0_6bba596ee6fb84e656d75c53902ecf01: resolution: {integrity: sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g==} engines: {node: '>=10'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - goober: 2.1.7 + goober: 2.1.7_csstype@2.6.19 react: 17.0.2 react-dom: 17.0.2_react@17.0.2 transitivePeerDependencies: @@ -4465,6 +4495,13 @@ packages: unist-util-visit-parents: 5.1.0 dev: false + /universal-cookie/4.0.4: + resolution: {integrity: sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==} + dependencies: + '@types/cookie': 0.3.3 + cookie: 0.4.2 + dev: false + /universalify/0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 340a306..a8da461 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -14,18 +14,25 @@ "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.", "Cancel": "Cancel", "Cannot preview {{path}}": "Cannot preview {{path}}", + "Change the raw file direct link to a URL ending with the extension of the file.": "Change the raw file direct link to a URL ending with the extension of the file.", "Check out <2>Microsoft's official explanation on the error message.": "Check out <2>Microsoft's official explanation on the error message.", "Clear all": "Clear all", "Clear all tokens?": "Clear all tokens?", "Cleared all tokens": "Cleared all tokens", "clearing them means that you will need to re-enter the passwords again.": "clearing them means that you will need to re-enter the passwords again.", + "Copied customised link to clipboard.": "Copied customised link to clipboard.", "Copied direct link to clipboard.": "Copied direct link to clipboard.", "Copied folder permalink.": "Copied folder permalink.", "Copied raw file permalink.": "Copied raw file permalink.", + "Copy custom link to clipboard": "Copy custom link to clipboard", "Copy direct link": "Copy direct link", "Copy folder permalink": "Copy folder permalink", "Copy raw file permalink": "Copy raw file permalink", "Copy the permalink to the file to the clipboard": "Copy the permalink to the file to the clipboard", + "Customise direct link": "Customise direct link", + "Customise link": "Customise link", + "Customised": "Customised", + "Default": "Default", "Do not pretend to be the site owner": "Do not pretend to be the site owner", "Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.": "Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.", "Download": "Download", @@ -47,6 +54,7 @@ "Failed to download selected files.": "Failed to download selected files.", "File is empty.": "File is empty.", "File size": "File size", + "Filename": "Filename", "Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ": "Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ", "Finished downloading folder.": "Finished downloading folder.", "Finished downloading selected files.": "Finished downloading selected files.", @@ -106,6 +114,7 @@ "Waiting for code...": "Waiting for code...", "Weibo": "Weibo", "Welcome to your new onedrive-vercel-index 🎉": "Welcome to your new onedrive-vercel-index 🎉", + "What is this?": "What is this?", "Where is the auth code? Did you follow step 2 you silly donut?": "Where is the auth code? Did you follow step 2 you silly donut?", "Whoops, looks like we got a problem: {{error}}.": "Whoops, looks like we got a problem: {{error}}." } diff --git a/public/locales/zh-CN/common.json b/public/locales/zh-CN/common.json index 62236ef..4d8f028 100644 --- a/public/locales/zh-CN/common.json +++ b/public/locales/zh-CN/common.json @@ -12,18 +12,25 @@ "Authorisation is required as no valid <2>access_token or <5>refresh_token is present on this deployed instance. Check the following configurations before proceeding with authorising onedrive-vercel-index with your own Microsoft account.": "本项目还没有设置有效的 <2>access_token 和 <5>refresh_token,需要进行授权。在继续对 onedrive-vercel-index 授权你的 Microsoft 帐号前,请检查一下下方的配置信息。", "Cancel": "取消", "Cannot preview {{path}}": "无法预览 {{path}}", + "Change the raw file direct link to a URL ending with the extension of the file.": "将文件直链接更改为以文件扩展名结尾的 URL。", "Check out <2>Microsoft's official explanation on the error message.": "请查阅 <2>Microsoft 官方解释 以获取详细的错误信息。", "Clear all": "清除所有密钥", "Clear all tokens?": "清除所有密钥?", "Cleared all tokens": "已清除所有密钥", "clearing them means that you will need to re-enter the passwords again.": "清除它们意味着下次访问时你需要重新输入密钥。", + "Copied customised link to clipboard.": "已复制自定义直链到剪贴板。", "Copied direct link to clipboard.": "已复制直链到剪贴板。", "Copied folder permalink.": "已复制文件夹永久链接。", "Copied raw file permalink.": "已复制文件永久链接。", + "Copy custom link to clipboard": "复制自定义链接", "Copy direct link": "复制文件直链", "Copy folder permalink": "复制文件夹永久链接", "Copy raw file permalink": "复制文件永久链接", "Copy the permalink to the file to the clipboard": "复制文件永久链接到剪贴板", + "Customise direct link": "自定义文件直链", + "Customise link": "自定义直链", + "Customised": "自定义链接", + "Default": "默认", "Do not pretend to be the site owner": "你不是网站所有者", "Don't worry, after storing them, onedrive-vercel-index will take care of token refreshes and updates after your site goes live.": "别担心,存储它们之后,onedrive-vercel-index 会在帮助你定时更新 token", "Download": "下载", @@ -45,6 +52,7 @@ "Failed to download selected files.": "下载选定文件失败。", "File is empty.": "文件为空。", "File size": "文件大小", + "Filename": "文件名", "Final step, click the button below to store these tokens persistently before they expire after {{minutes}} minutes {{seconds}} seconds. ": "最后一步,在这些 tokens 于 {{minutes}} 分钟 {{seconds}} 秒后失效前,点击下方按钮以永久存储这些 tokens", "Finished downloading folder.": "下载文件夹成功。", "Finished downloading selected files.": "下载选定文件成功。", @@ -102,6 +110,7 @@ "Waiting for code...": "等待授权码…", "Weibo": "微博", "Welcome to your new onedrive-vercel-index 🎉": "欢迎来到你崭新的 onedrive-vercel-index 🎉", + "What is this?": "这是什么?", "Where is the auth code? Did you follow step 2 you silly donut?": "授权码呢?你遵守了第 2 步吗?你这个傻瓜甜甜圈!o( ̄ヘ ̄o#)", "Whoops, looks like we got a problem: {{error}}.": "Whoops,看来我们遇到了一个问题:{{error}}" }