Compare commits

..

19 Commits

Author SHA1 Message Date
icechen 57daf38d3d update 2022-03-08 04:22:28 +08:00
icechen f3baaa174f update 2022-03-07 04:22:35 +08:00
icechen fdc5516388 update 2022-03-02 20:07:12 +08:00
icechen b55af04082 update 2022-03-02 04:52:36 +08:00
icechen be3da0a45d update 2022-03-01 20:22:44 +08:00
icechen 473d3e55c7 update 2022-03-01 10:24:10 +08:00
icechen fc35a1f0c8 update 2022-03-01 10:22:10 +08:00
icechen 5d479d322b update 2022-02-28 19:43:15 +08:00
icechen c5868a8ebf update 2022-02-28 00:39:52 +08:00
icechen 1d44a7eca4 update 2022-02-27 04:18:44 +08:00
icechen 907409203c update 2022-02-25 02:38:45 +08:00
icechen 8b8cfc64bc update 2022-02-24 19:25:05 +08:00
icechen 8c78b93e6e update 2022-02-24 03:51:45 +08:00
icechen 5a1c0cfffb update 2022-02-23 19:50:27 +08:00
icechen a7bd08ec79 update 2022-02-22 20:23:44 +08:00
icechen 3044a43a89 update 2022-02-22 00:53:55 +08:00
icechen 26cb0eaef1 demo compile 2022-02-20 02:34:07 +08:00
icechen 78b2b4b753 update 2022-02-18 20:03:12 +08:00
icechen 65ac000169 uodate 2022-02-17 04:21:21 +08:00
36 changed files with 10910 additions and 11725 deletions

11608
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,11 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.4.2",
"@mui/material": "^5.4.2",
"@reduxjs/toolkit": "^1.8.0",
"@testing-library/jest-dom": "^5.16.2", "@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2", "@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
@ -10,10 +15,15 @@
"@types/node": "^16.11.25", "@types/node": "^16.11.25",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"axios": "^0.26.0",
"filesize": "^8.0.7",
"mtproton": "6.0.0", "mtproton": "6.0.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"redux": "^4.1.2",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@ -41,5 +51,11 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"devDependencies": {} "devDependencies": {
"autoprefixer": "^10.4.2",
"postcss": "^8.4.7",
"prettier-plugin-tailwindcss": "^0.1.8",
"redux-devtools": "^3.7.0",
"tailwindcss": "^3.0.23"
}
} }

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -26,7 +26,7 @@
--> -->
<title>React App</title> <title>React App</title>
</head> </head>
<body> <body style="margin: 0">
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <!--
@ -39,5 +39,13 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"
/>
</body> </body>
</html> </html>

View File

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,48 +0,0 @@
import React, { useEffect, useState } from "react";
import "./App.css";
import Client from "mtproton/envs/browser";
function App() {
let [countriesList, setcountriesList] = useState([]);
useEffect(() => {
let client = new Client({
api_id: 18987971,
api_hash: "fcfd9e6ed3f9e48a360bb57cc0d59d98",
});
client.call("help.getCountriesList").then((result: any) => {
console.log("country:", result);
result.countries.map((country: any) => {
// @ts-ignore
setcountriesList((oldArray) => [
...oldArray,
country.default_name + ": " + country.country_codes[0].country_code,
]);
return true;
});
});
}, []);
// client
// .invoke({
// _: "getCountryCode",
// })
// .then((res) => {
// console.log(res);
// });
return (
<div className="App">
{countriesList.map((country: any, index) => {
return (
<div key={index}>
{country}
<br />
</div>
);
})}
</div>
);
}
export default App;

View File

@ -0,0 +1,214 @@
import style from "./style.module.css";
import { useEffect, useMemo, useRef, useState } from "react";
import { Close } from "@mui/icons-material";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
} from "@mui/material";
import fileSize from "filesize";
import SendIcon from "@mui/icons-material/Send";
import { inputFile, sendFile, uploadBigFile } from "../../telegram/telegram";
import { useDispatch } from "react-redux";
import { addFileOfStorage } from "../../store/fileList";
enum Status {
normal,
addFile,
receive,
}
interface Props {
isSignIn: boolean;
onNotSignIn: () => void;
}
export function HomeButton(props: Props) {
let [status, setStatus] = useState(Status.normal);
let [boxStyle, setBoxStyle] = useState([style.box]);
let [isReceive, setIsReceive] = useState(false);
let [waitedFileList, setWaitedFileList] = useState<any[]>([]);
let [uploadRatio, setUploadRatio] = useState(0);
let [isFinish, setIsFinish] = useState(false);
let [startUploadTime, setStartUploadTime] = useState(0);
let uploadSpeed = useMemo(() => {
if (waitedFileList.length === 0) {
return "0/s";
}
let size = waitedFileList[0].size;
if (uploadRatio === 0) {
return "0/s";
}
return (
fileSize((size * uploadRatio * 10) / (Date.now() - startUploadTime)) +
"/s"
);
}, [startUploadTime, uploadRatio, waitedFileList]);
const dispatch = useDispatch();
let onNormal = () => {
setStatus(Status.normal);
};
let onAddFile = () => {
setStatus(Status.addFile);
};
let onReceive = () => {
setStatus(Status.receive);
};
useEffect(() => {
switch (status) {
case Status.normal:
setBoxStyle([style.box]);
break;
case Status.addFile:
setBoxStyle([style.box, style.boxActivePlus]);
break;
case Status.receive:
setBoxStyle([style.box, style.boxActiveReceive]);
break;
}
}, [status]);
let addFileInput = useRef<HTMLInputElement>(null);
let OnChangeFileInput = (e: any) => {
setWaitedFileList(e.target.files);
};
let handleFileUpload = () => {
setUploadRatio(1);
setStartUploadTime(Date.now());
const reader = new FileReader();
reader.onload = function (event) {
if (event.target && event.target.result) {
let bytes = event.target.result;
uploadBigFile(bytes as ArrayBuffer, setUploadRatio)
.then((file) => {
console.log("uploadFile ret:", file);
sendFile(
// @ts-ignore
inputFile(file.file_id, file.total_part, waitedFileList[0].name),
waitedFileList[0].type
).then((result) => {
console.log("sendFile ret:", result);
result.updates[1].message.media.document.file_name =
waitedFileList[0].name;
dispatch(
addFileOfStorage(result.updates[1].message.media.document)
);
setIsFinish(true);
});
})
.catch((error) => {
console.log("uploadFile error:", error);
alert(error);
});
}
};
reader.readAsArrayBuffer(waitedFileList[0]);
};
return (
<div className={boxStyle.join(" ")}>
{!isReceive ? (
<>
<div className={style.plus}></div>
<div
className={style.addFile}
onMouseEnter={onAddFile}
onMouseLeave={onNormal}
onClick={() => addFileInput.current?.click()}
>
<input
ref={addFileInput}
type="file"
style={{ display: "none" }}
onChange={OnChangeFileInput}
/>
</div>
<button
className={style.receive}
onMouseEnter={onReceive}
onMouseLeave={onNormal}
onClick={() => {
if (props.isSignIn) {
setIsReceive(true);
} else {
props.onNotSignIn();
}
}}
>
</button>
</>
) : (
<>
<input
placeholder={"输入6位传输口令获取文件"}
style={{}}
className={isReceive ? style.receiveInput : style.receiveInputNone}
/>
<IconButton
className={style.receiveInputClose}
aria-label="delete"
onClick={() => {
setIsReceive(false);
}}
>
<Close />
</IconButton>
</>
)}
<Dialog open={waitedFileList.length !== 0}>
<DialogTitle></DialogTitle>
<DialogContent sx={{ minWidth: 300 }}>
?
<br />
<br />
: {waitedFileList.length > 0 ? waitedFileList[0].name : ""}
<br />
:{" "}
{waitedFileList.length > 0 ? fileSize(waitedFileList[0].size) : ""}
<br />
{uploadRatio !== 0 && <>: {uploadRatio.toFixed(2)}%</>}
<br />
{uploadRatio !== 0 && (
<>: {(Date.now() - startUploadTime) / 1000}s</>
)}
<br />
{uploadRatio !== 0 && <>: {uploadSpeed}</>}
<br />
{isFinish && <></>}
</DialogContent>
<DialogActions>
<Button
variant="outlined"
onClick={() => {
setWaitedFileList([]);
setUploadRatio(0);
setIsFinish(false);
}}
>
</Button>
{uploadRatio === 0 && (
<Button
variant="contained"
onClick={handleFileUpload}
endIcon={<SendIcon />}
>
</Button>
)}
</DialogActions>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,99 @@
body {
/*background-color: yellow;*/
}
.box {
width: 400px;
height: 80px;
border-radius: 50px;
background-color: #ffffff;
display: flex;
align-items: center;
position: relative;
box-shadow: 0 0 15px #a7a7a7;
transition: 0.5s;
color: #252525;
}
.boxActivePlus {
transform: translateX(-20px);
}
.boxActiveReceive {
transform: translateX(20px);
}
.plus {
font-size: 40px;
margin-left: 30px;
height: 100px;
text-align: center;
line-height: 100px;
cursor: pointer;
}
.addFile {
font-size: 24px;
padding-left: 30px;
margin-right: 140px;
font-weight: bold;
flex-grow: 1;
cursor: pointer;
}
.receive {
position: absolute;
height: 60px;
width: 120px;
right: 10px;
font-size: 18px;
color: black;
border-radius: 35px;
border: 0;
background-color: #f2f2f2;
}
.receive:hover {
background-color: #fdda65;
cursor: pointer;
}
.receiveInput {
position: absolute;
width: 348px;
height: 48px;
background-color: white;
font-size: 18px;
color: #252525;
border-radius: 35px;
margin: 0 0 0 10px;
padding: 0 0 0 20px;
line-height: 60px;
outline: none;
border: #f1f1f1 solid 6px;
transition: 0.3s;
-webkit-user-drag: none;
right: 10px;
}
.receiveInputNone {
width: 120px;
transition: 0.3s;
}
.receiveInput:hover {
border: #fdda65 solid 6px;
}
.receiveInput:active {
border: 0;
}
.receiveInput::selection {
background-color: #ffd95a;
}
.receiveInputClose {
position: absolute !important;
right: 20px;
}

View File

@ -0,0 +1,27 @@
import { Button } from "@mui/material";
import style from "./style.module.css";
import { Link } from "react-router-dom";
import React from "react";
interface Props {
isSignIn: boolean;
onOpenSignIn: () => void;
className?: string;
}
export function HomeMenu(props: Props) {
return (
<div className={props.className}>
<Button className={style.text}></Button>
{props.isSignIn ? (
<Link className={style.text} to={`/dashboard`}>
<Button className={style.text}></Button>
</Link>
) : (
<Button className={style.text} onClick={props.onOpenSignIn}>
/
</Button>
)}
</div>
);
}

View File

@ -0,0 +1,4 @@
.text {
color: #252525 !important;
text-decoration: none;
}

View File

@ -0,0 +1,156 @@
import { Button, Paper, Snackbar, TextField } from "@mui/material";
import style from "./style.module.css";
import CountriesInput, { Country } from "../signin/countriesInput";
import { useMemo, useState } from "react";
import { sendSignInCode, signIn } from "../../telegram/telegram";
import { getMe } from "../../telegram/user";
import { login } from "../../store/user";
import { useDispatch } from "react-redux";
export function HomeSignIn() {
const dispatch = useDispatch();
let [country, setCountry] = useState({} as Country | null);
let [phone, setPhone] = useState("");
let [phoneCode, setPhoneCode] = useState("");
let [openSuccess, setOpenSuccess] = useState(false);
const phoneNumber = useMemo(
() => (country ? country.country_code : "") + phone,
[country, phone]
);
let [codeHash, setCodeHash] = useState("");
let onSendCode = () => {
sendSignInCode(phoneNumber)
.then((phone_code_hash: string) => {
setCodeHash(phone_code_hash);
})
.catch((error: any) => {
console.log("error", error);
});
};
let onSignIn = () => {
signIn(phoneNumber, phoneCode, codeHash)
.then((user: any) => {
console.log("user", user);
setOpenSuccess(true);
getMe()
.then(function (user) {
// 已登录
dispatch(login(user));
})
.catch(function (error) {
if (error.code === 401) {
// Unauthorized
}
console.log(error);
});
})
.catch((error: any) => {
console.log("error", error);
});
};
return (
<Paper
elevation={0}
sx={{
width: "400px",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
border: "1px solid #e0e0e0",
padding: "50px 0",
}}
>
<h1 className={style.h1}> / </h1>
<div className={style.describe}>/</div>
{codeHash === "" ? (
<>
<div className={style.signInInput}>
<CountriesInput
onChange={(value) => {
setCountry(value);
}}
/>
<TextField
style={{ marginTop: "10px" }}
label="Phone Number"
className={style.phone}
placeholder={"手机号码"}
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</div>
<Button
size={"large"}
style={{ alignSelf: "flex-end", marginRight: "50px" }}
variant="contained"
onClick={onSendCode}
>
</Button>
</>
) : (
<>
<div className={style.signInInput}>
<TextField
style={{ marginTop: "10px" }}
label="Auth Code"
className={style.phone}
placeholder={"验证码"}
value={phoneCode}
onChange={(e) => setPhoneCode(e.target.value)}
/>
</div>
<div style={{ display: "flex", flexDirection: "row" }}>
<Button
size={"large"}
style={{ alignSelf: "flex-end", marginRight: "50px" }}
variant="contained"
onClick={() => {
setCodeHash("");
}}
>
</Button>
<Button
size={"large"}
style={{ alignSelf: "flex-end", marginRight: "50px" }}
variant="contained"
onClick={onSignIn}
>
</Button>
</div>
</>
)}
<div className={style.explain}>
<h1 className={style.h1}></h1>
<div className={style.explainContent}>
TelegramAPI
Telegram
telegram 使
</div>
</div>
<div className={style.describe}>
<br />
</div>
<Snackbar
anchorOrigin={{ vertical: "top", horizontal: "right" }}
open={openSuccess}
onClose={() => {
setOpenSuccess(false);
}}
message="登录成功"
/>
</Paper>
);
}

View File

@ -0,0 +1,39 @@
.h1 {
font-size: 36px;
margin: 0;
padding: 0;
font-weight: bold;
}
.describe {
color: #a7a7a7;
font-size: 14px;
text-align: center;
}
.signInInput {
display: flex;
flex-direction: column;
margin-top: 30px;
margin-bottom: 30px;
width: 300px;
}
.phone {
/*margin-left: 50px;*/
}
.explain {
margin: 30px 0;
padding: 30px 0;
width: 100%;
text-align: center;
background-color: #f7f7f7;
}
.explainContent {
padding: 20px;
font-size: 14px;
text-align: left;
letter-spacing: 2px;
}

View File

@ -0,0 +1,82 @@
import { Autocomplete, Box, TextField } from "@mui/material";
import React, { useEffect, useState } from "react";
import Telegram from "../../telegram/telegram";
export interface Country {
default_name: string;
country_code: string;
// patterns?: string;
iso2: string;
}
function CountriesInput(props: {
onChange: (country: Country | null) => void;
}) {
let [countriesList, setCountriesList] = useState([] as Country[]);
useEffect(() => {
Telegram.call("help.getCountriesList", undefined, undefined, true).then(
(result: any) => {
let resp: Country[] = [];
result.countries.map((country: any) => {
resp.push({
default_name: country.default_name,
country_code: country.country_codes[0].country_code,
// patterns:
// country.country_codes[0]?.patterns.length > 0
// ? country.country_codes[0]?.patterns[0]
// : "",
iso2: country.iso2,
});
return true;
});
setCountriesList(resp);
}
);
}, []);
return (
<Autocomplete
disablePortal
autoHighlight
id="country-select"
options={countriesList.sort(function (a, b) {
const nameA = a.default_name.toUpperCase(); // ignore upper and lowercase
const nameB = b.default_name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
})}
sx={{ width: 300 }}
onChange={(_event, value) => {
props.onChange(value as Country);
}}
getOptionLabel={(option) => option.default_name}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.iso2.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.iso2.toLowerCase()}.png 2x`}
alt=""
/>
{option.default_name} ({option.iso2}) +{option.country_code}
</Box>
)}
renderInput={(params) => <TextField {...params} label="Country" />}
/>
);
}
export default CountriesInput;

View File

@ -0,0 +1,185 @@
import CountriesInput, { Country } from "./countriesInput";
import { Button, Input, InputAdornment, Paper, TextField } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";
import { Download, Upload } from "@mui/icons-material";
import React, { useState } from "react";
import { User } from "../../user";
import Telegram, {
downloadFile,
inputFile,
sendFile,
uploadBigFile,
} from "../../telegram/telegram";
function SignIn() {
let [phone, setPhone] = useState("");
let [code, setCode] = useState("");
let [phoneCodeHash, setPhoneCodeHash] = useState("");
let [country, setCountry] = useState({} as Country | null);
let [user, setUser] = useState({} as User);
let [fileDocument, setFileDocument] = useState({} as any);
const [fileList, setFileList] = useState([]);
let sendCode = () => {
console.log("phone:", phone);
Telegram.call("auth.sendCode", {
phone_number: (country ? country.country_code : "") + phone,
settings: {
_: "codeSettings",
},
})
.then((result: any) => {
console.log("auth.sendCode:", result);
setPhoneCodeHash(result.phone_code_hash);
})
.catch((error: any) => {
console.log("auth.sendCode:", error);
});
};
let login = () => {
console.log("auth code:", code);
console.log("auth hash code:", phoneCodeHash);
Telegram.call("auth.signIn", {
phone_number: (country ? country.country_code : "") + phone,
phone_code_hash: phoneCodeHash,
phone_code: code,
})
.then((result: any) => {
console.log("auth.signIn:", result);
setUser(result.user);
SendMessage();
})
.catch((error: any) => {
console.log("auth.signIn:", error);
});
};
let SendMessage = () => {
Telegram.call("messages.sendMessage", {
peer: {
_: "inputPeerSelf",
},
message: "ice",
random_id: (10000 + Math.random() * (100000 - 10000))
.toFixed()
.toString(),
})
.then((result: any) => {
console.log("messages.sendMessage:", result);
})
.catch((error: any) => {
console.log("messages.sendMessage:", error);
});
};
let uploadFile = () => {
console.log("uploadFile:", fileList);
// @ts-ignore
uploadBigFile(fileList[0]?.bytes)
.then((file) => {
console.log("uploadFile ret:", file);
sendFile(
// @ts-ignore
inputFile(file.file_id, file.total_part, fileList[0].name),
// @ts-ignore
fileList[0].type
).then((result) => {
console.log("sendFile ret:", result);
setFileDocument(result.updates[1].message.media);
});
})
.catch((error) => {
console.log("uploadFile error:", error);
alert(error);
});
};
let changeFile = (e: any) => {
console.log(e.target.files);
const reader = new FileReader();
reader.onload = function (event) {
if (event.target && event.target.result) {
let bytes = event.target.result;
console.log(bytes);
e.target.files[0].bytes = bytes;
setFileList(e.target.files);
}
};
reader.readAsArrayBuffer(e.target.files[0]);
};
let download = () => {
console.log("download:", fileDocument);
downloadFile(fileDocument).catch((error) => {
console.log("download error:", error);
alert(error);
});
};
return (
<Paper
elevation={3}
sx={{
padding: "150px 50px",
position: "absolute",
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
height: "40%",
width: "40%",
}}
>
<CountriesInput
onChange={(country) => {
console.log(country);
setCountry(country);
}}
/>
<TextField
label={"Phone Number"}
InputProps={{
startAdornment: (
<InputAdornment position="start">
{country && country.country_code
? "+" + country.country_code
: ""}
</InputAdornment>
),
}}
value={phone}
onChange={(e) => {
console.log(e.target.value);
setPhone(e.target.value);
}}
/>
<TextField
label={"Auth Code"}
value={code}
onChange={(e) => {
console.log(e.target.value);
setCode(e.target.value);
}}
/>
{phoneCodeHash === "" ? (
<Button variant={"contained"} onClick={sendCode}>
</Button>
) : (
<Button variant={"contained"} onClick={login} endIcon={<SendIcon />}>
</Button>
)}
<Input type="file" onChange={changeFile} />
<Button variant={"contained"} onClick={uploadFile} endIcon={<Upload />}>
</Button>
{/*<Button variant={"contained"} onClick={download} endIcon={<Download />}>*/}
{/* 下载*/}
{/*</Button>*/}
</Paper>
);
}
export default SignIn;

8
src/global.d.ts vendored
View File

@ -1 +1,9 @@
declare module "mtproton/envs/browser"; declare module "mtproton/envs/browser";
declare module "mtproton/src/utils/common";
declare module "mtproton/src/index";
declare module "mtproton/envs/browser/sha1";
declare module "mtproton/envs/browser/sha256";
declare module "mtproton/envs/browser/pbkdf2";
declare module "mtproton/envs/browser/transport";
declare module "mtproton/envs/browser/get-random-bytes";
declare module "mtproton/envs/browser/get-local-storage";

View File

@ -1,13 +1,3 @@
body { @tailwind base;
margin: 0; @tailwind components;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @tailwind utilities;
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -1,14 +1,57 @@
import React from 'react'; // @ts-nocheck
import ReactDOM from 'react-dom';
import './index.css'; import React from "react";
import App from './App'; import ReactDOM from "react-dom";
import reportWebVitals from './reportWebVitals'; import reportWebVitals from "./reportWebVitals";
import Dashboard from "./pages/dashboard";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/home";
import { Provider } from "react-redux";
import store from "./store";
import { updateOfStorage } from "./store/fileList";
import Test from "./pages/test";
import "./index.css";
store.dispatch(updateOfStorage());
// eslint-disable-next-line no-extend-native
Date.prototype.Format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
S: this.getMilliseconds(), //毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(
RegExp.$1,
(this.getFullYear() + "").substr(4 - RegExp.$1.length)
);
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt))
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length)
);
return fmt;
};
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App /> <Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/test" element={<Test />} />
</Routes>
</BrowserRouter>
</Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -0,0 +1,170 @@
import style from "./style.module.css";
import { Link } from "react-router-dom";
import React, { useState } from "react";
import {
Divider,
IconButton,
Menu,
MenuItem,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import { useSelectorFileList } from "../../store/fileList";
import fileSize from "filesize";
import { downloadFile } from "../../telegram/telegram";
enum ContentType {
File,
Share,
SafeBox,
Recycled,
}
function Login() {
let [type, setType] = useState(ContentType.File);
return (
<div
style={{
width: "100vw",
height: "100vh",
display: "flex",
alignItems: "center",
}}
>
<div className={style.menu}>
<li onClick={() => setType(ContentType.File)}></li>
<li onClick={() => setType(ContentType.Share)}></li>
<li onClick={() => setType(ContentType.SafeBox)}></li>
<li onClick={() => setType(ContentType.Recycled)}></li>
<Link to={`/`}>
<div className={style.logo}>LOGO</div>
</Link>
</div>
<div className={style.content}>
{type === ContentType.File && <File />}
{type === ContentType.Share && <Share />}
{type === ContentType.SafeBox && <SafeBox />}
{type === ContentType.Recycled && <Recycled />}
</div>
</div>
);
}
function File() {
let [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
let [waitDownloadFile, setWaitDownloadFile] = useState(null);
let fileList = useSelectorFileList();
const open = Boolean(anchorEl);
let handleClick = (index: number) => {
return (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("index", index);
setWaitDownloadFile(fileList[index]);
setAnchorEl(event.currentTarget);
};
};
let handleClose = () => {
setAnchorEl(null);
};
let Download = () => {
downloadFile(waitDownloadFile)
.then((res) => {
console.log("res", res);
})
.catch((error) => {
console.log("download error:", error);
alert(error);
});
};
return (
<div
style={{
display: "flex",
alignItems: "center",
height: "100%",
width: "100%",
}}
>
<Paper sx={{ padding: "100px", width: "100%" }}>
<TableContainer sx={{ backgroundColor: "#f2f2f2", height: 500 }}>
<Table aria-label={"文件列表"}>
<TableHead>
<TableRow>
<TableCell sx={{ width: "70%" }}></TableCell>
<TableCell></TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{fileList.map((item: any, index: number) => {
return (
<TableRow key={item.id} sx={{ cursor: "pointer" }}>
<TableCell sx={{ position: "relative" }}>
{item.attributes[0].file_name}
<IconButton
sx={{
position: "absolute",
right: 20,
top: "50%",
transform: "translate(-50%,-50%)",
}}
onClick={handleClick(index)}
>
<MoreHorizIcon />
</IconButton>
</TableCell>
<TableCell>
{
// @ts-ignore
new Date(item.date * 1000).Format("yyyy-MM-dd hh:mm:ss")
}
</TableCell>
<TableCell>{fileSize(item.size)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Paper>
<Menu
open={open}
id="basic-menu"
anchorEl={anchorEl}
onClose={handleClose}
>
<MenuItem onClick={Download}></MenuItem>
<MenuItem onClick={handleClose}></MenuItem>
<Divider />
<MenuItem onClick={handleClose}></MenuItem>
<MenuItem onClick={handleClose}></MenuItem>
<MenuItem onClick={handleClose}></MenuItem>
<MenuItem onClick={handleClose}></MenuItem>
<MenuItem onClick={handleClose}></MenuItem>
</Menu>
</div>
);
}
function Share() {
return <div></div>;
}
function SafeBox() {
return <div></div>;
}
function Recycled() {
return <div></div>;
}
export default Login;

View File

@ -0,0 +1,41 @@
.menu {
display: flex;
flex-direction: column;
list-style: none;
width: 350px;
height: 100vh;
background-color: #f2f2f2;
align-items: center;
justify-content: center;
}
.menu li {
width: 350px;
line-height: 100px;
text-align: center;
border-top: #949494 1px solid;
cursor: pointer;
}
.menu > li:first-child {
border-top: none;
}
.menu > li:hover {
background-color: #e6e6e6;
}
.logo {
position: absolute;
top: 20px;
left: 20px;
font-size: 48px;
cursor: pointer;
}
.content {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
}

View File

@ -0,0 +1,75 @@
import { useEffect, useState } from "react";
import { getMe } from "../../telegram/user";
import { HomeButton } from "../../component/homeButton";
import style from "./style.module.css";
import { Box, Drawer } from "@mui/material";
import { HomeMenu } from "../../component/homeMenu";
import { HomeSignIn } from "../../component/homeSignIn";
import { useDispatch } from "react-redux";
import { useSelectorIsLoggedIn, useSelectorUser } from "../../store/user";
import { login } from "../../store/user";
function Home() {
// let [isSignIn, setIsSignIn] = useState(false);
const dispatch = useDispatch();
let isSignIn = useSelectorIsLoggedIn();
let [isOpenSignIn, setIsOpenSignIn] = useState(false);
let user = useSelectorUser();
useEffect(
function () {
getMe()
.then(function (user) {
// 已登录
dispatch(login(user));
})
.catch(function (error) {
if (error.code === 401) {
// Unauthorized
}
console.log(error);
});
},
[dispatch]
);
return (
<div className={style.box}>
<div className={style.homeButton}>
<HomeButton
isSignIn={isSignIn}
onNotSignIn={() => {
setIsOpenSignIn(true);
}}
/>
</div>
<HomeMenu
className={style.homeMenu}
isSignIn={isSignIn}
onOpenSignIn={() => {
setIsOpenSignIn(true);
}}
/>
{user.user && <div>{user.user.username}</div>}
<Drawer
anchor={"right"}
open={isOpenSignIn && !isSignIn}
onClose={() => {
setIsOpenSignIn(false);
}}
ModalProps={{
keepMounted: true,
}}
transitionDuration={500}
>
<Box sx={{ width: "600px", height: "100%" }}>
<div className={style.homeSignIn}>
<HomeSignIn />
</div>
</Box>
</Drawer>
</div>
);
}
export default Home;

View File

@ -0,0 +1,27 @@
.homeButton {
position: absolute;
top: 50%;
left: 88px;
}
.homeSignIn {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.homeMenu {
position: absolute;
right: 0;
margin-top: 20px;
margin-right: 50px;
padding: 5px 10px;
background-color: white;
border-radius: 5px;
}
body {
background-color: #eae4de;
}

View File

@ -0,0 +1,38 @@
export default function Test() {
const people = [
{
name: "Calvin Hawkins",
email: "calvin.hawkins@example.com",
image:
"https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
},
{
name: "Kristen Ramos",
email: "kristen.ramos@example.com",
image:
"https://images.unsplash.com/photo-1550525811-e5869dd03032?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
},
{
name: "Ted Fox",
email: "ted.fox@example.com",
image:
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
},
];
return (
<ul className="divide-y divide-gray-200 w-screen h-screen box-border align-middle flex flex-col justify-center">
{people.map((person) => (
<li
key={person.email}
className="py-4 flex hover:bg-amber-50 hover:scale-105 hover:animate-none transition-all cursor-pointer animate-pulse select-none container pl-20"
>
<img className="h-10 w-10 rounded-full" src={person.image} alt="" />
<div className="ml-5">
<p className=" font-medium text-lg text-gray-900">{person.name}</p>
<p className="text-sm text-gray-500 slashed-zero">{person.email}</p>
</div>
</li>
))}
</ul>
);
}

29
src/storage.ts 100644
View File

@ -0,0 +1,29 @@
function getLocalStorage() {
return {
set(key: string, value: string) {
return window.localStorage.setItem(key, value);
},
get(key: string) {
return window.localStorage.getItem(key);
},
setObject(key: string, value: any) {
console.log("setObject", key, value);
return window.localStorage.setItem(key, JSON.stringify(value));
},
getObject(key: string, defaultValue?: any) {
let value = window.localStorage.getItem(key);
if (value) {
return JSON.parse(value);
}
if (defaultValue) {
return defaultValue;
}
return {};
},
};
}
export default getLocalStorage;

13
src/store.ts 100644
View File

@ -0,0 +1,13 @@
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import userReducer from "./store/user";
import fileListReducer from "./store/fileList";
export default configureStore({
reducer: {
user: userReducer,
fileList: fileListReducer,
},
middleware: getDefaultMiddleware({
serializableCheck: false,
}),
});

View File

@ -0,0 +1,27 @@
import { createSlice } from "@reduxjs/toolkit";
import { useSelector } from "react-redux";
import storage from "../storage";
const FileList = createSlice({
name: "fileList",
initialState: [] as any[],
reducers: {
updateOfStorage: (state) => {
state = storage().getObject("fileList", []);
return state;
},
addFileOfStorage: (state, action) => {
state.push(action.payload);
storage().setObject("fileList", state);
},
},
});
export function useSelectorFileList() {
return useSelector((state: any) => state.fileList);
}
export const { updateOfStorage, addFileOfStorage } = FileList.actions;
export default FileList.reducer;

32
src/store/user.ts 100644
View File

@ -0,0 +1,32 @@
import { createSlice } from "@reduxjs/toolkit";
import { useSelector } from "react-redux";
const User = createSlice({
name: "user",
initialState: {
isLoggedIn: false,
user: {},
},
reducers: {
login: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload;
},
logout: (state) => {
state.isLoggedIn = false;
state.user = {};
},
},
});
export function useSelectorUser() {
return useSelector((state: any) => state.user.user);
}
export function useSelectorIsLoggedIn() {
return useSelector((state: any) => state.user.isLoggedIn);
}
export const { login, logout } = User.actions;
export default User.reducer;

View File

@ -0,0 +1,245 @@
import Client from "mtproton/envs/browser";
import { rand_id } from "../utils/rand";
import { obj } from "../utils/log";
import makeMTProto from "mtproton/src/index";
import SHA1 from "mtproton/envs/browser/sha1";
import SHA256 from "mtproton/envs/browser/sha256";
import PBKDF2 from "mtproton/envs/browser/pbkdf2";
import Transport from "mtproton/envs/browser/transport";
import getRandomBytes from "mtproton/envs/browser/get-random-bytes";
import getLocalStorage from "mtproton/envs/browser/get-local-storage";
class TelegramHelper {
private client: any;
constructor(appID: number, appHash: string, custom: boolean = false) {
if (!custom) {
this.client = new Client({
api_id: appID,
api_hash: appHash,
test: false,
});
} else {
let createTransport = function (dc: any, crypto: any) {
return new Transport(dc, crypto);
};
const MTProto = makeMTProto({
SHA1,
SHA256,
PBKDF2,
getRandomBytes,
getLocalStorage,
createTransport,
});
this.client = new MTProto({
api_id: appID,
api_hash: appHash,
test: true,
});
}
// this.client.setDefaultDc(2);
}
async call(
method: string,
params?: object,
options?: object,
hideLog: boolean = false
): Promise<any> {
try {
!hideLog && console.dir(`${method} req\n${obj(params)}`);
options = {
...options,
};
let resp = await this.client.call(method, params, options);
!hideLog && console.dir(`${method} resp\n${obj(resp)}`);
return resp;
} catch (error) {
!hideLog && console.log(`${method} error:`, error);
// @ts-ignore
const { error_code, error_message } = error;
// if (error_code === 420) {
// const seconds = Number(error_message.split('FLOOD_WAIT_')[1]);
// const ms = seconds * 1000;
//
// await sleep(ms);
//
// return this.call(method, params, options);
// }
if (error_code === 303) {
const [, dcIdAsString] = error_message.split("_MIGRATE_");
const dcId = Number(dcIdAsString);
// if (type === "PHONE") {
await this.client.setDefaultDc(dcId);
// } else {
// Object.assign(options, { dcId });
// }
return this.call(method, params, options);
}
return Promise.reject(error);
}
}
}
async function uploadBigFile(
bytes: ArrayBuffer,
callback?: (ratio: number) => void
): Promise<{ file_id: number; total_part: number }> {
let file_id = rand_id();
console.log("file_id", file_id);
let uploadBytes = new Uint8Array(bytes);
console.log("uploading file", bytes);
const file_total_parts = Math.ceil(uploadBytes.length / 524288);
console.log("file_total_parts", file_total_parts);
for (let i = 0; i < file_total_parts; i++) {
console.log("push part: ", i);
try {
let tempBytes = uploadBytes.slice(i * 524288, (i + 1) * 524288);
if (i === file_total_parts - 1) {
tempBytes = uploadBytes.slice(i * 524288);
}
console.log("tempBytes", tempBytes);
let finished = await Telegram.call("upload.saveBigFilePart", {
file_id: file_id,
file_part: i,
file_total_parts: file_total_parts,
bytes: tempBytes,
});
console.log("finished", finished);
if (callback) {
callback(((i + 1) / file_total_parts) * 100);
}
} catch (error) {
return Promise.reject(error);
}
}
console.log("uploaded");
return { file_id: file_id, total_part: file_total_parts };
}
function inputFile(file_id: number, part_number: number, file_name: string) {
return {
_: "inputFileBig",
id: file_id,
parts: part_number,
name: file_name,
};
}
function sendFile(inputFile: any, type: string) {
let random_id = rand_id();
console.log("random_id", random_id);
return Telegram.call("messages.sendMedia", {
peer: {
_: "inputPeerSelf",
},
media: {
_: "inputMediaUploadedDocument",
file: inputFile,
mime_type: type,
attributes: [
{
_: "documentAttributeFilename",
file_name: inputFile.name,
},
],
},
random_id: rand_id(),
message: "test",
});
}
async function downloadFile(fileDocument: any) {
let partSize = 1024 * 1024;
const file_total_parts = Math.ceil(fileDocument.size / partSize);
let results = [];
for (let i = 0; i < file_total_parts; i++) {
try {
let result = await Telegram.call("upload.getFile", {
location: {
_: "inputDocumentFileLocation",
id: fileDocument.id,
access_hash: fileDocument.access_hash,
file_reference: fileDocument.file_reference,
thumb_size: "",
},
limit: partSize,
offset: partSize * i,
});
console.log("result", result);
results.push(result);
} catch (error) {
return Promise.reject(error);
}
}
console.log("messages.getDocument:", results);
let bytes = new Uint8Array(fileDocument.size);
for (let i = 0; i < results.length; i++) {
for (let j = 0; j < results[i].bytes.length; j++) {
bytes[i * partSize + j] = results[i].bytes[j];
}
}
let downloadBytes = bytes;
console.log("downloadBytes", downloadBytes);
let blob = new Blob([downloadBytes], {
type: fileDocument.mime_type,
});
let url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = fileDocument.file_name;
a.click();
return Promise.resolve(true);
}
async function sendSignInCode(phone: string): Promise<string> {
try {
let result = await Telegram.call("auth.sendCode", {
phone_number: phone,
settings: {
_: "codeSettings",
},
});
return Promise.resolve(result.phone_code_hash);
} catch (error: any) {
return Promise.reject(error);
}
}
async function signIn(phone: string, code: string, phone_code_hash: string) {
try {
let result = await Telegram.call("auth.signIn", {
phone_number: phone,
phone_code_hash: phone_code_hash,
phone_code: code,
});
return Promise.resolve(result);
} catch (error: any) {
return Promise.reject(error);
}
}
const appID = 18987971;
const appHash = "fcfd9e6ed3f9e48a360bb57cc0d59d98";
let Telegram = new TelegramHelper(appID, appHash);
export default Telegram;
export {
uploadBigFile,
inputFile,
sendFile,
downloadFile,
sendSignInCode,
signIn,
};

View File

@ -0,0 +1,9 @@
import Telegram from "./telegram";
async function getMe() {
return await Telegram.call("users.getFullUser", {
id: { _: "inputUserSelf" },
});
}
export { getMe };

29
src/user.ts 100644
View File

@ -0,0 +1,29 @@
export interface Status {
_: string;
was_online: number;
}
export interface User {
_: string;
flags: number;
self: boolean;
contact: boolean;
mutual_contact: boolean;
deleted: boolean;
bot: boolean;
bot_chat_history: boolean;
bot_nochats: boolean;
verified: boolean;
restricted: boolean;
min: boolean;
bot_inline_geo: boolean;
support: boolean;
scam: boolean;
apply_min_photo: boolean;
fake: boolean;
id: string;
access_hash: string;
first_name: string;
phone: string;
status: Status;
}

View File

@ -0,0 +1,20 @@
function ArraybufferToStr(buffer: ArrayBuffer) {
let uint8 = new Uint8Array(buffer);
let decoder = new TextDecoder("utf8");
return decoder.decode(uint8);
}
// function ab2str(buf: ArrayBuffer) {
// return String.fromCharCode.apply(null, buf as Uint16Array);
// }
// function str2ab(str: string) {
// let buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char
// let bufView = new Uint16Array(buf);
// for (let i = 0, strLen = str.length; i < strLen; i++) {
// bufView[i] = str.charCodeAt(i);
// }
// return buf;
// }
//
export default ArraybufferToStr;

12
src/utils/log.ts 100644
View File

@ -0,0 +1,12 @@
const maxToLog = 10240;
export function obj(obj: any): string {
if (!obj) {
return "null";
}
let s = JSON.stringify(obj, null, 2);
if (s.length > maxToLog) {
s = s.substr(0, maxToLog) + "...";
}
return s;
}

View File

@ -0,0 +1,9 @@
function between(min: number, max: number): number {
return Math.floor(Math.random() * (max - min) + min);
}
function rand_id(): number {
return between(100000, 999999);
}
export { between, rand_id };

View File

@ -0,0 +1,9 @@
module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {},
plugins: [],
};

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "src",
"target": "es5", "target": "es5",
"lib": [ "lib": [
"dom", "dom",

9225
yarn.lock 100644

File diff suppressed because it is too large Load Diff