创建项目

master
ZGGSONG 3 years ago
commit cc1a4d2b5f

9
.gitignore vendored

@ -0,0 +1,9 @@
/uploads
/main
/*.exe
*.log
/go-demo-*
/FileSync*
node_modules
/build
*.syso

@ -0,0 +1,41 @@
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react"
],
"settings": {
"react": {
"version": "detect"
},
},
"rules": {
"no-unused-vars": ["error", { "vars": "local", "args": "none", "ignoreRestSiblings": true }],
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/prop-types": "off"
},
"overrides": [
{
"files": ["*.{ts,tsx}"],
"rules": {
},
"parser": "@typescript-eslint/parser",
}
],
};

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/src/images/synk.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>同步传</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<script src="//at.alicdn.com/t/font_2846367_4w90lpzz304.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,39 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build --base ./",
"lint": "eslint --ext .jsx --ext .js --ext .ts --ext .tsx ./src",
"serve": "vite preview"
},
"dependencies": {
"axios": "0.22.0",
"classnames": "2.3.1",
"history": "4.10.1",
"lodash": "4.17.21",
"query-string": "^7.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-is": "17.0.2",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"styled-components": "5.3.1",
"swr": "1.0.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/react": "17.0.24",
"@types/react-dom": "17.0.9",
"@types/styled-components": "^5.1.14",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vitejs/plugin-react": "^1.0.2",
"@vitejs/plugin-react-refresh": "1.3.6",
"eslint": "^7.32.0",
"eslint-plugin-react": "^7.26.1",
"typescript": "^4.4.3",
"vite": "2.6.3"
}
}

@ -0,0 +1,7 @@
import styled from "styled-components";
export const Center = styled.div`
display: flex;
flex-direction: ${({ virtical }) => virtical ? 'column' : 'row'};
justify-content: center;
align-items: center;
`;

@ -0,0 +1,55 @@
import { createContext } from "react";
import { unmountComponentAtNode, render, createPortal } from "react-dom";
import styled from "styled-components";
import { AppContext } from "../shared/app_context";
const DialogOverlay = styled.div`
position: fixed; z-index: 10; background: rgba(0,0,0,0.5); width: 100%;
height: 100%; left: 0; top: 0;
`;
const DialogContent = styled.div`
position: fixed; z-index: 11; min-width: 120px; min-height: 40px;
max-width: 100%; max-height: 100%; background: white; top: 50%; left: 50%;
transform: translate(-50%, -50%);
border-radius: 8px;
`;
export const Dialog = ({ container, onClickOverlay, children }) => {
return createPortal(
<>
<DialogOverlay onClick={onClickOverlay} />
<DialogContent>{children}</DialogContent>
</>,
container ?? document.body
);
};
let lastDiv = null
const close = (div) => {
if (!div) return
unmountComponentAtNode(div);
div.remove();
lastDiv = null
};
export const createDialog = (content, options = {}) => {
close(lastDiv)
const { closeOnClickOverlay, context } = options;
const div = lastDiv = document.createElement("div");
div.className = "tempApp";
document.body.append(div);
const onClickOverlay = () => {
if (closeOnClickOverlay) {
close(div);
}
};
render(
<AppContext.Provider value={context}>
<Dialog container={div} onClickOverlay={onClickOverlay}>
{content}
</Dialog>
</AppContext.Provider>,
div
);
return () => close(div);
};

@ -0,0 +1,23 @@
import { Center } from "./center";
import styled from "styled-components";
const _Loading = styled(Center)`
flex-direction: column;
padding: 8px;
> svg {
width: 40px;
height: 40px;
margin: 16px;
}
`;
export const Loading = ({ children, className }) => {
return (
<_Loading className={className}>
<svg className="spin">
<use xlinkHref="#icon-loading" />
</svg>
<p>{children}</p>
</_Loading>
);
};

@ -0,0 +1,32 @@
import React, { useEffect, useState } from "react";
import { Loading } from "./loading";
import styled from "styled-components";
import { prefetch } from "../shared/prefetch";
const MyLoading = styled(Loading)`
width: 256px;
height: 256px;
`;
export const Qrcode = ({ content }) => {
const [image, setImage] = useState(null);
content = encodeURIComponent(content);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!content) return;
const url = `/api/v1/qrcodes?content=${content}`;
setLoading(true);
prefetch(url)
.then(
() => setImage(<img width="256" height="256" src={url} />),
(e) => setError(e)
)
.finally(() => setLoading(false));
}, [content]);
return loading ? (
<MyLoading>加载中</MyLoading>
) : error ? (
<div>加载二维码出错{JSON.stringify(error)}</div>
) : (
image
);
};

@ -0,0 +1,4 @@
import styled from "styled-components"
export const Space = styled.div`
height: ${({ x2, x3 }) => x2 ? 16 * 2 : x3 ? 16 * 3 : 16}px;
`

@ -0,0 +1,7 @@
import { useLocation } from "react-router";
import qs from 'query-string'
export const useQuery = () => {
const location = useLocation();
const parsed = qs.parse(location.search)
return parsed
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1632304369475" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18016" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512 938.666667c235.648 0 426.666667-191.018667 426.666667-426.666667S747.648 85.333333 512 85.333333 85.333333 276.352 85.333333 512s191.018667 426.666667 426.666667 426.666667zM372.48 458.666667A149.418667 149.418667 0 0 1 512 362.666667c46.634667 0 86.912 19.797333 114.048 52.48a32 32 0 1 0 49.237333-40.96C635.904 326.826667 577.578667 298.666667 512 298.666667a212.949333 212.949333 0 0 0-170.666667 85.333333v-32a32 32 0 0 0-64 0v138.666667c0 17.664 14.336 32 32 32H426.666667a32 32 0 0 0 0-64H372.48z m310.186667 213.333333V640a213.888 213.888 0 0 1-333.696 10.112 32 32 0 1 1 48.725333-41.472 149.888 149.888 0 0 0 249.258667-32.64H597.333333a32 32 0 0 1 0-64h117.333334a32 32 0 0 1 32 32v128a32 32 0 0 1-64 0z" p-id="18017" fill="#f60"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,9 @@
import { v4 as uuidv4 } from 'uuid';
let clientId = localStorage.getItem('clientId')
if (clientId?.length !== 36) {
clientId = uuidv4();
localStorage.setItem('clientId', clientId)
}
export { clientId }

@ -0,0 +1,4 @@
import './client_id'
export const init = () => {
}

@ -0,0 +1,78 @@
import React, { useContext, useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { Router, Route, Switch, Redirect } from "react-router";
import { Home } from "./pages/home";
import { Downloads } from "./pages/downloads";
import { ThemeProvider } from "styled-components";
import { GlobalStyle } from './shared/global_style'
import { history } from "./shared/history";
import { init } from "./initializers";
import { getWsClient } from "./shared/ws_client";
import { clientId } from "./initializers/client_id";
import { createDialog } from "./components/dialog";
import { showUploadFileSuccessDialog, showUploadTextSuccessDialog } from "./pages/home/components";
import { http } from "./shared/http";
import _ from "lodash";
import { AppContext } from "./shared/app_context";
const theme = {
borderColor: "#333",
highlightColor: "#f5b70d",
};
const Main = () => {
init()
const addressesRef = useRef(null);
const context = { addressesRef }
useEffect(async () => {
const {
data: { addresses },
} = await http
.get("/api/v1/addresses")
.catch((e) => Promise.reject(e));
addressesRef.current = _.uniq(addresses.concat("127.0.0.1"));
}, []);
useEffect(() => {
getWsClient().then(c => {
c.onMessage(data => {
const { url, type } = data
if (data.clientId !== clientId) {
const content = (addr) => addr && `http://${addr}:27149/static/downloads?type=${type}&url=${encodeURIComponent(`http://${addr}:27149${url}`)}`
if (type === 'text') {
showUploadTextSuccessDialog({ context, content });
} else {
showUploadFileSuccessDialog({ context, content });
}
}
})
})
}, [])
return <ThemeProvider theme={theme}>
<GlobalStyle />
<AppContext.Provider value={context}>
<Router history={history}>
<Switch>
<Redirect exact from="/" to="/message" />
<Route exact path="/downloads">
<Downloads />
</Route>
<Route path="/">
<Home />
</Route>
<Route path="*">
<div>404</div>
</Route>
</Switch>
</Router>
</AppContext.Provider>
</ThemeProvider>
}
ReactDOM.render(
<React.StrictMode>
<Main />
</React.StrictMode>,
document.getElementById("root")
);

@ -0,0 +1,93 @@
import { useQuery } from "../hooks/use_query";
import { BigTextarea, Button, Header, Layout } from "./home/components";
import styled from "styled-components";
import { Center } from "../components/center";
import { Space } from "../components/space";
import { useState } from "react";
import { useRef } from "react";
import { useEffect } from "react";
import { http } from "../shared/http";
import { history } from "../shared/history";
export const Downloads = () => {
const query = useQuery()
const type = normalizeType(query.type)
const [text, setText] = useState("")
useEffect(() => {
if (type === "text") {
http.get(query.url).then(({ data }) => {
setText(data)
})
}
}, [type])
let node = null
switch (type) {
case 'text':
node = (
<div>
<BigTextarea readOnly value={text} />
<Space />
<Center virtical>
<Button>请手动复制上面文本</Button>
</Center>
</div>
)
break;
case 'file':
node = (
<Center virtical>
<a href={query.url}>
<svg><use xlinkHref="#icon-file"></use></svg>
<Space />
<Center>
<Button>点击下载文件</Button>
</Center>
</a>
</Center>
)
break;
case 'image':
node = (
<Center>
<a href={query.url}>
<Picture src={query.url} />
<Center>
<Button>长按或点击即可下载图片</Button>
</Center>
</a>
</Center>
)
break;
}
const onClickUpload = () => {
history.push("/")
}
return (
<Layout>
<Header>同步传</Header>
{node}
<Space x3 />
<Center>
<Button onClick={onClickUpload}>我也要上传</Button>
</Center>
</Layout>
)
};
const Picture = styled.img`
border: 2px solid ${({ theme }) => theme.borderColor};
margin: 16px;
`
const P = styled.p`
margin: 8px 0;
`
const normalizeType = type => {
if (/^image\/.*$/.test(type)) {
return 'image'
} else if (/^text(\/.*)?$/.test(type)) {
return 'text'
} else {
return 'file'
}
}

@ -0,0 +1,26 @@
import { Switch, Route } from "react-router";
import { Header, Layout } from "./home/components";
import { UploadTextForm } from "./home/upload_text_form";
import { UploadFileForm } from "./home/upload_file_form";
import { nav } from "./home/nav";
import { UploadScreenshotForm } from "./home/upload_screenshot_form";
export function Home() {
return (
<Layout>
<Header>同步传</Header>
{nav}
<Switch>
<Route exact path="/message">
<UploadTextForm />
</Route>
<Route exact path="/file">
<UploadFileForm />
</Route>
<Route exact path="/screenshot">
<UploadScreenshotForm />
</Route>
</Switch>
</Layout>
);
}

@ -0,0 +1,157 @@
import { createDialog } from "../../components/dialog";
import styled from "styled-components";
import React, { useContext, useState } from "react";
import { Qrcode } from "../../components/qrcode";
import { Loading } from "../../components/loading";
import { AppContext } from "../../shared/app_context";
import { Center } from "../../components/center";
import { http } from "../../shared/http";
import { getWsClient } from "../../shared/ws_client";
import { clientId } from "../../initializers/client_id";
import { Space } from "../../components/space";
export const Layout = styled.div`
min-height: 100vh; display: flex; align-items: stretch; flex-direction: column;
padding: 0 16px; margin: 0 auto;
@media (min-width: 414px) {
max-width: 600px;
}
`;
export const Header = styled.h1`
margin-top: 48px;
margin-bottom: 32px;
text-align: center;
`;
export const BigTextarea = styled.textarea`
width: 100%;
min-height: 160px;
line-height: 20px;
&.draging {
border-color: red;
}
`;
export const Button = styled.button`
min-height: 40px;
padding: 0 60px;
border: 2px solid #666;
background: #f5b70d;
border-radius: 8px;
cursor: pointer;
`;
export const Form = styled.form`
&> .row {
margin: 16px 0;
}
`;
const Span = styled.span`
margin-right: 8px;
`;
const Label = styled.label`
display: flex; padding: 4px 0;
justify-content: flex-start; align-items: center;
min-height: 40px; white-space: nowrap;
`;
const H2 = styled.h2`
font-weight: bold; font-size: 24px;
margin-bottom: 16px;
`
const P = styled.p`
a {text-decoration: underline;}
`
const UploadSuccessDialog = ({ content, onClose }) => {
const [address, setAddress] = useState(localStorage.getItem("address") || "");
const context = useContext(AppContext);
const addressesRef = context?.addressesRef ?? null
const onChange = (e) => {
setAddress(e.target.value);
localStorage.setItem("address", e.target.value);
};
content = typeof content === "string" ? content : content(address)
return (
<Pop>
<H2>上传成功</H2>
{addressesRef.current ?
<div>
<P>
Windows 用户在防火墙入站规则中开通 27149 端口<a href="https://jingyan.baidu.com/article/09ea3ede7311dec0afde3977.html" target="_blank" rel="noreferrer">教程</a>
</P>
<P>
<Label>
<Span>请选择手机可以访问的局域网IP</Span>
<select value={address} onChange={onChange}>
<option value="" disabled>
- 请选择 -
</option>
{addressesRef.current?.map((string) => (
<option key={string}>{string}</option>
))}
</select>
</Label>
</P>
</div>
: null
}
<Center>
{content ? <Qrcode content={content} /> : null}
</Center>
<Center>
{content ? <a href={content}> 手机扫码 点击下载</a> : null}
</Center>
<Space />
<Center>
<Button onClick={onClose}>关闭</Button>
</Center>
</Pop>
);
};
export const showUploadTextSuccessDialog = ({ context, content }) => {
const close = createDialog(
<UploadSuccessDialog content={content} onClose={() => close()} />,
{ context }
);
};
export const showUploadFileSuccessDialog = ({ context, content }) => {
const close = createDialog(
<UploadSuccessDialog content={content} onClose={() => close()} />,
{ context }
);
};
export const showUploadFailDialog = () => {
return createDialog(
<Pop>
<div>上传失败</div>
<button onClick={() => close()}>关闭</button>
</Pop>
);
};
export const showUploadingDialog = () => {
return createDialog(<Loading>上传中</Loading>);
};
const Pop = styled.div`
padding: 16px;
`;
const notifyPc = (response, type) => {
getWsClient().then(c => {
c.send({ clientId, type, url: response.data.url })
})
return response
}
export const uploadText = (text) => {
return http.post("/api/v1/texts", {
raw: text
}).then(r => notifyPc(r, 'text'))
}
export const uploadFile = (blob) => {
const formData = new FormData();
formData.append("raw", blob);
return http({
method: "post",
url: "/api/v1/files",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
}).then(r => notifyPc(r, 'file'))
};

@ -0,0 +1,36 @@
import { NavLink } from "react-router-dom";
import styled from "styled-components";
const Nav = styled.nav`
text-align: center;
> ul {
display: flex; border-top: 1px solid ${({ theme }) => theme.borderColor};
border-left: 1px solid ${({ theme }) => theme.borderColor};
> li { flex-grow: 1; border-bottom: 1px solid #333;
border-right: 1px solid ${({ theme }) => theme.borderColor};
> a { display: block; padding: 8px 0;
&.selected{ background: ${({ theme }) => theme.highlightColor} }
}
}
}
`;
export const nav = (
<Nav>
<ul>
<li>
<NavLink to="/message" activeClassName="selected">
传消息
</NavLink>
</li>
<li>
<NavLink to="/file" activeClassName="selected">
传文件
</NavLink>
</li>
<li>
<NavLink to="/screenshot" activeClassName="selected">
传截图
</NavLink>
</li>
</ul>
</Nav>
);

@ -0,0 +1,79 @@
import React, { useContext, useState } from "react";
import styled from "styled-components";
import {
Form,
showUploadingDialog,
showUploadFileSuccessDialog,
uploadFile,
} from "../../pages/home/components";
import { AppContext } from "../../shared/app_context";
export const UploadFileForm = () => {
const context = useContext(AppContext);
const [boxClass, setBoxClass] = useState("default");
const onDragOver = (e) => {
e.preventDefault();
setBoxClass("dragging");
};
const onDragLeave = (e) => {
setBoxClass("default");
};
const onDrop = async (e) => {
e.preventDefault();
const file = e.dataTransfer?.items?.[0]?.getAsFile();
if (!file) return;
const type = file.type || "unknown";
showUploadingDialog();
const { data: { url } } = await uploadFile(file);
showUploadFileSuccessDialog({
context,
content: (addr) =>
addr &&
`http://${addr}:27149/static/downloads?type=${type}&url=${encodeURIComponent(
`http://${addr}:27149${url}`
)}`,
});
};
const onChange = async (e) => {
const file = e.target?.files?.[0];
if (!file) return;
const type = file.type || "unknown";
showUploadingDialog();
const { data: { url } } = await uploadFile(file);
showUploadFileSuccessDialog({
context,
content: (addr) =>
addr &&
`http://${addr}:27149/static/downloads?type=${type}&url=${encodeURIComponent(
`http://${addr}:27149${url}`
)}`,
});
};
return (
<Form className="uploadForm">
<div className="row">
<Box
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
className={boxClass}
>
<FileInput type="file" value="" onChange={onChange} />
<p>点击打开文件 拖拽文件至此</p>
</Box>
</div>
</Form>
);
};
const Box = styled.div`
&.dragging {
border-color: ${({ theme }) => theme.highlightColor};
color: ${({ theme }) => theme.highlightColor};
}
min-height: 160px; border: 2px dashed ${({ theme }) => theme.borderColor};
position: relative; overflow: hidden; display: flex; justify-content: center; align-items: center; border-radius: 8px;
`;
const FileInput = styled.input`
position: absolute; right: 0; top: 0; width: 100%; height: 100%;
opacity: 0; cursor: pointer;
`;

@ -0,0 +1,66 @@
import React, { useContext, useEffect } from "react";
import styled from "styled-components";
import {
Form,
showUploadingDialog,
showUploadFileSuccessDialog,
uploadFile,
} from "../../pages/home/components";
import { AppContext } from "../../shared/app_context";
export const UploadScreenshotForm = () => {
const context = useContext(AppContext);
const _uploadFile = async (file) => {
if (!file) return;
const type = file.type || "unknown";
showUploadingDialog();
const { data: { url } } = await uploadFile(file);
showUploadFileSuccessDialog({
context,
content: (addr) =>
addr &&
`http://${addr}:27149/static/downloads?type=${type}&url=${encodeURIComponent(
`http://${addr}:27149${url}`
)}`,
});
}
const onPaste = (e) => {
const { items: [item] } = e.clipboardData;
_uploadFile(item?.getAsFile())
};
useEffect(() => {
window.addEventListener("paste", onPaste);
return () => {
window.removeEventListener("paste", onPaste);
};
}, []);
const onChange = async (e) => {
_uploadFile(e.target?.files?.[0])
};
return (
<Form className="uploadForm">
<div className="row">
<Box>
<FileInput
type="file"
value=""
onChange={onChange}
accept="image/*;capture=camera"
/>
<p>点击选择图片 直接粘贴图片</p>
</Box>
</div>
</Form>
);
};
const Box = styled.div`
&.dragging {
border-color: ${({ theme }) => theme.highlightColor};
color: ${({ theme }) => theme.highlightColor};
}
min-height: 160px; border: 2px dashed ${({ theme }) => theme.borderColor};
position: relative; overflow: hidden; display: flex; justify-content: center; align-items: center; border-radius: 8px;
`;
const FileInput = styled.input`
position: absolute; right: 0; top: 0; width: 100%; height: 100%;
opacity: 0; cursor: pointer;
`;

@ -0,0 +1,37 @@
import React, { useContext, useState } from "react";
import {
BigTextarea,
Button,
Form,
showUploadingDialog,
showUploadTextSuccessDialog,
uploadText,
} from "../../pages/home/components";
import { AppContext } from "../../shared/app_context";
import { Center } from "../../components/center";
export const UploadTextForm = () => {
const context = useContext(AppContext);
const [text, setText] = useState("");
const onSubmit = async (e) => {
e.preventDefault();
showUploadingDialog();
const { data: { url } } = await uploadText(text)
showUploadTextSuccessDialog({
context, content: (addr) => addr && `http://${addr}:27149/static/downloads?type=text&url=http://${addr + ":27149" + encodeURIComponent(url)}`
});
};
return (
<Form className="uploadForm" onSubmit={onSubmit}>
<div className="row">
<BigTextarea
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
<Center className="row">
<Button type="submit">上传</Button>
</Center>
</Form>
);
};

@ -0,0 +1,3 @@
import React from 'react'
export const AppContext = React.createContext({ addressesRef: null })

@ -0,0 +1,28 @@
import { createGlobalStyle, keyframes } from "styled-components";
const spin = keyframes`
0% {
transform: rotate(0deg);
}
100%{
transform: rotate(360deg);
}
`;
export const GlobalStyle = createGlobalStyle`
* { box-sizing: border-box; padding: 0; margin: 0; }
*::before, *::after {box-sizing: border-box;}
body {
font-size: 16px;
font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
}
a {text-decoration: none; color: inherit;}
img {max-width: 100%; max-height: 100%; }
input, button {font: inherit;}
ul, ol {list-style: none; }
img{vertical-align: middle;}
:focus{ outline: none; }
// helpers
.spin {
animation: ${spin} 2s linear infinite;
}
`;

@ -0,0 +1,2 @@
import { createBrowserHistory } from "history";
export const history = createBrowserHistory({ basename: "/static/" });

@ -0,0 +1,4 @@
import axios from "axios";
export const http = axios.create({
timeout: 5000
});

@ -0,0 +1,12 @@
export const prefetch = (src) => {
return new Promise((resolve, reject) => {
const picture = new Image()
picture.onload = () => {
resolve(picture)
}
picture.onerror = (error) => {
reject(error)
}
picture.src = src
})
}

@ -0,0 +1,28 @@
const url = `ws://${window.location.hostname}:27149/ws`;
const wsClient = new WebSocket(url);
class WsClient {
constructor(client) {
this.client = client
}
send(data) {
this.client.send(JSON.stringify(data))
}
onMessage(fn) {
this.client.onmessage = ({ data }) => {
fn(JSON.parse(data))
}
}
}
const promise = new Promise((resolve, reject) => {
wsClient.onopen = () => {
resolve(new WsClient(wsClient))
}
setTimeout(() => {
reject(new Error('get ws connection timeout'))
}, 10000)
})
export const getWsClient = () => promise

@ -0,0 +1 @@
/// <reference types="vite/client" />

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": ["./src"]
}

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:27149/',
changeOrigin: true,
},
}
}
})

File diff suppressed because it is too large Load Diff

@ -0,0 +1,22 @@
module github.com/zggsong/FileSync
go 1.17
require github.com/gin-gonic/gin v1.7.7
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)

@ -0,0 +1,54 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

@ -0,0 +1,24 @@
package main
import (
"embed"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
)
//go:embed frontend/dist/*
var FS embed.FS
func main() {
go func() {
gin.SetMode(gin.DebugMode)
router := gin.Default()
staticFiles, _ := fs.Sub(FS, "frontend/dist")
router.StaticFS("/static", http.FS(staticFiles))
}()
}
Loading…
Cancel
Save