setup vscode for consistent JS formatting

This commit is contained in:
Igor Katson 2023-12-07 14:11:12 +00:00
parent a641717245
commit ec63e1cef7
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
15 changed files with 1675 additions and 1338 deletions

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -1,21 +1,27 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>rqbit web 4.0.0-beta.0</title>
<link rel="icon" type="image/svg+xml" href="assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd" crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
integrity="sha384-4LISF5TTJX/fLmGSxO53rV4miRxdg84mZsxmO8Rx5jGtp/LbrixFETvWa5a6sESd"
crossorigin="anonymous"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.tsx"></script>
</body>
</html>

View file

@ -304,6 +304,21 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View file

@ -12,6 +12,7 @@
"devDependencies": {
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.16",
"prettier": "3.1.0",
"typescript": "^5.3.2",
"vite": "^4.5.1"
}
@ -653,6 +654,21 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
"integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
"dev": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",

View file

@ -14,6 +14,7 @@
"devDependencies": {
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.16",
"prettier": "3.1.0",
"typescript": "^5.3.2",
"vite": "^4.5.1"
}

View file

@ -12,14 +12,14 @@ export interface TorrentFile {
// Interface for the Torrent Details API response
export interface TorrentDetails {
info_hash: string,
info_hash: string;
files: Array<TorrentFile>;
}
export interface AddTorrentResponse {
id: number | null;
details: TorrentDetails;
output_folder: string,
output_folder: string;
seen_peers?: Array<string>;
}
@ -66,34 +66,33 @@ export interface LiveTorrentStats {
time_remaining: {
human_readable: string;
duration?: {
secs: number,
}
secs: number;
};
} | null;
}
export const STATE_INITIALIZING = 'initializing';
export const STATE_PAUSED = 'paused';
export const STATE_LIVE = 'live';
export const STATE_ERROR = 'error';
export const STATE_INITIALIZING = "initializing";
export const STATE_PAUSED = "paused";
export const STATE_LIVE = "live";
export const STATE_ERROR = "error";
export interface TorrentStats {
state: 'initializing' | 'paused' | 'live' | 'error',
error: string | null,
progress_bytes: number,
finished: boolean,
total_bytes: number,
state: "initializing" | "paused" | "live" | "error";
error: string | null;
progress_bytes: number;
finished: boolean;
total_bytes: number;
live: LiveTorrentStats | null;
}
export interface ErrorDetails {
id?: number,
method?: string,
path?: string,
status?: number,
statusText?: string,
text: string,
};
id?: number;
method?: string;
path?: string;
status?: number;
statusText?: string;
text: string;
}
export type Duration = number;
@ -117,12 +116,14 @@ export interface AddTorrentOptions {
preferred_id?: number | null;
}
export interface RqbitAPI {
listTorrents: () => Promise<ListTorrentsResponse>,
getTorrentDetails: (index: number) => Promise<TorrentDetails>,
listTorrents: () => Promise<ListTorrentsResponse>;
getTorrentDetails: (index: number) => Promise<TorrentDetails>;
getTorrentStats: (index: number) => Promise<TorrentStats>;
uploadTorrent: (data: string | File, opts?: AddTorrentOptions) => Promise<AddTorrentResponse>;
uploadTorrent: (
data: string | File,
opts?: AddTorrentOptions
) => Promise<AddTorrentResponse>;
pause: (index: number) => Promise<void>;
start: (index: number) => Promise<void>;

View file

@ -1,15 +1,29 @@
import { AddTorrentResponse, ErrorDetails, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats } from "./api-types";
import {
AddTorrentResponse,
ErrorDetails,
ListTorrentsResponse,
RqbitAPI,
TorrentDetails,
TorrentStats,
} from "./api-types";
// Define API URL and base path
const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : '';
const apiUrl =
window.origin === "null" || window.origin === "http://localhost:3031"
? "http://localhost:3030"
: "";
const makeRequest = async (method: string, path: string, data?: any): Promise<any> => {
const makeRequest = async (
method: string,
path: string,
data?: any
): Promise<any> => {
console.log(method, path);
const url = apiUrl + path;
const options: RequestInit = {
method,
headers: {
'Accept': 'application/json',
Accept: "application/json",
},
body: data,
};
@ -17,7 +31,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
let error: ErrorDetails = {
method: method,
path: path,
text: ''
text: "",
};
let response: Response;
@ -25,7 +39,7 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
try {
response = await fetch(url, options);
} catch (e) {
error.text = 'network error';
error.text = "network error";
return Promise.reject(error);
}
@ -36,7 +50,10 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
const errorBody = await response.text();
try {
const json = JSON.parse(errorBody);
error.text = json.human_readable !== undefined ? json.human_readable : JSON.stringify(json, null, 2);
error.text =
json.human_readable !== undefined
? json.human_readable
: JSON.stringify(json, null, 2);
} catch (e) {
error.text = errorBody;
}
@ -44,24 +61,25 @@ const makeRequest = async (method: string, path: string, data?: any): Promise<an
}
const result = await response.json();
return result;
}
};
export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
listTorrents: (): Promise<ListTorrentsResponse> => makeRequest('GET', '/torrents'),
listTorrents: (): Promise<ListTorrentsResponse> =>
makeRequest("GET", "/torrents"),
getTorrentDetails: (index: number): Promise<TorrentDetails> => {
return makeRequest('GET', `/torrents/${index}`);
return makeRequest("GET", `/torrents/${index}`);
},
getTorrentStats: (index: number): Promise<TorrentStats> => {
return makeRequest('GET', `/torrents/${index}/stats/v1`);
return makeRequest("GET", `/torrents/${index}/stats/v1`);
},
uploadTorrent: (data, opts): Promise<AddTorrentResponse> => {
let url = '/torrents?&overwrite=true';
let url = "/torrents?&overwrite=true";
if (opts?.list_only) {
url += '&list_only=true';
url += "&list_only=true";
}
if (opts?.only_files != null) {
url += `&only_files=${opts.only_files.join(',')}`;
url += `&only_files=${opts.only_files.join(",")}`;
}
if (opts?.peer_opts?.connect_timeout) {
url += `&peer_connect_timeout=${opts.peer_opts.connect_timeout}`;
@ -70,34 +88,34 @@ export const API: RqbitAPI & { getVersion: () => Promise<string> } = {
url += `&peer_read_write_timeout=${opts.peer_opts.read_write_timeout}`;
}
if (opts?.initial_peers) {
url += `&initial_peers=${opts.initial_peers.join(',')}`;
url += `&initial_peers=${opts.initial_peers.join(",")}`;
}
if (opts?.output_folder) {
url += `&output_folder=${opts.output_folder}`;
}
if (typeof data === 'string') {
url += '&is_url=true';
if (typeof data === "string") {
url += "&is_url=true";
}
return makeRequest('POST', url, data)
return makeRequest("POST", url, data);
},
pause: (index: number): Promise<void> => {
return makeRequest('POST', `/torrents/${index}/pause`);
return makeRequest("POST", `/torrents/${index}/pause`);
},
start: (index: number): Promise<void> => {
return makeRequest('POST', `/torrents/${index}/start`);
return makeRequest("POST", `/torrents/${index}/start`);
},
forget: (index: number): Promise<void> => {
return makeRequest('POST', `/torrents/${index}/forget`);
return makeRequest("POST", `/torrents/${index}/forget`);
},
delete: (index: number): Promise<void> => {
return makeRequest('POST', `/torrents/${index}/delete`);
return makeRequest("POST", `/torrents/${index}/delete`);
},
getVersion: async (): Promise<string> => {
const r = await makeRequest('GET', '/');
const r = await makeRequest("GET", "/");
return r.version;
}
}
},
};

View file

@ -1,27 +1,33 @@
import { StrictMode, useEffect, useState } from "react";
import ReactDOM from 'react-dom/client';
import ReactDOM from "react-dom/client";
import { RqbitWebUI, APIContext, customSetInterval } from "./rqbit-web";
import { API } from "./http-api";
const RootWithVersion = () => {
let [title, setTitle] = useState<string>("rqbit web UI");
useEffect(() => {
const refreshVersion = () => API.getVersion().then((version) => {
const refreshVersion = () =>
API.getVersion().then(
(version) => {
setTitle(`rqbit web UI - v${version}`);
return 10000;
}, (e) => {
},
(e) => {
return 1000;
});
return customSetInterval(refreshVersion, 0)
}, [])
}
);
return customSetInterval(refreshVersion, 0);
}, []);
return <StrictMode>
return (
<StrictMode>
<APIContext.Provider value={API}>
<RqbitWebUI title={title} />
</APIContext.Provider>
</StrictMode>;
}
</StrictMode>
);
};
ReactDOM.createRoot(document.getElementById('app') as HTMLInputElement).render(
ReactDOM.createRoot(document.getElementById("app") as HTMLInputElement).render(
<RootWithVersion />
);

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,10 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>rqbit web 4.0.0-beta.0</title>
<link rel="icon" type="image/svg+xml" href="assets/logo.svg">
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="/src/styles/bootstrap.min.css" />
<link rel="stylesheet" href="/src/styles/bootstrap-icons.css" />
@ -15,5 +14,4 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,31 +1,45 @@
import { AddTorrentResponse, ListTorrentsResponse, RqbitAPI, TorrentDetails, TorrentStats, ErrorDetails } from "./rqbit-webui-src/api-types";
import {
AddTorrentResponse,
ListTorrentsResponse,
RqbitAPI,
TorrentDetails,
TorrentStats,
ErrorDetails,
} from "./rqbit-webui-src/api-types";
import { InvokeArgs, invoke } from "@tauri-apps/api/tauri"
import { InvokeArgs, invoke } from "@tauri-apps/api/tauri";
interface InvokeErrorResponse {
error_kind: string,
human_readable: string,
status: number,
status_text: string,
error_kind: string;
human_readable: string;
status: number;
status_text: string;
}
function errorToUIError(path: string): (e: InvokeErrorResponse) => Promise<never> {
function errorToUIError(
path: string
): (e: InvokeErrorResponse) => Promise<never> {
return (e: InvokeErrorResponse) => {
console.log(e);
let reason: ErrorDetails = {
method: 'INVOKE',
method: "INVOKE",
path: path,
text: e.human_readable,
status: e.status,
statusText: e.status_text
statusText: e.status_text,
};
return Promise.reject(reason);
}
};
}
export async function invokeAPI<Response>(name: string, params?: InvokeArgs): Promise<Response> {
export async function invokeAPI<Response>(
name: string,
params?: InvokeArgs
): Promise<Response> {
console.log("invoking", name, params);
const result = await invoke<Response>(name, params).catch(errorToUIError(name));
const result = await invoke<Response>(name, params).catch(
errorToUIError(name)
);
console.log(result);
return result;
}
@ -35,11 +49,11 @@ async function readFileAsBase64(file: File): Promise<string> {
const reader = new FileReader();
reader.onload = function (event) {
const base64String = (event?.target?.result as string)?.split(',')[1];
const base64String = (event?.target?.result as string)?.split(",")[1];
if (base64String) {
resolve(base64String);
} else {
reject(new Error('Failed to read file as base64.'));
reject(new Error("Failed to read file as base64."));
}
};
@ -65,10 +79,13 @@ export const API: RqbitAPI = {
uploadTorrent: async function (data, opts): Promise<AddTorrentResponse> {
if (data instanceof File) {
let contents = await readFileAsBase64(data);
return await invokeAPI<AddTorrentResponse>("torrent_create_from_base64_file", {
return await invokeAPI<AddTorrentResponse>(
"torrent_create_from_base64_file",
{
contents,
opts: opts ?? {},
});
}
);
}
return await invokeAPI<AddTorrentResponse>("torrent_create_from_url", {
url: data,
@ -86,5 +103,5 @@ export const API: RqbitAPI = {
},
delete: function (id: number): Promise<void> {
return invokeAPI<void>("torrent_action_delete", { id });
}
}
},
};

View file

@ -45,6 +45,6 @@ export interface RqbitDesktopConfig {
}
export interface CurrentDesktopState {
config: RqbitDesktopConfig | null,
configured: boolean,
config: RqbitDesktopConfig | null;
configured: boolean;
}

View file

@ -6,14 +6,15 @@ import { invokeAPI } from "./api";
import { ErrorDetails } from "./rqbit-webui-src/api-types";
const FormCheck: React.FC<{
label: string,
name: string,
checked: boolean,
onChange: (e: any) => void,
disabled?: boolean,
help?: string,
label: string;
name: string;
checked: boolean;
onChange: (e: any) => void;
disabled?: boolean;
help?: string;
}> = ({ label, name, checked, onChange, disabled, help }) => {
return <Form.Group as={Row} controlId={name} className="mb-3">
return (
<Form.Group as={Row} controlId={name} className="mb-3">
<Form.Label className="col-4">{label}</Form.Label>
<div className="col-8">
<Form.Check
@ -26,18 +27,20 @@ const FormCheck: React.FC<{
</div>
{help && <div className="form-text">{help}</div>}
</Form.Group>
}
);
};
const FormInput: React.FC<{
label: string,
name: string,
value: string | number,
inputType: string,
onChange: (e: any) => void,
disabled?: boolean,
help?: string
label: string;
name: string;
value: string | number;
inputType: string;
onChange: (e: any) => void;
disabled?: boolean;
help?: string;
}> = ({ label, name, value, inputType, onChange, disabled, help }) => {
return <Form.Group as={Row} controlId={name} className="mb-3">
return (
<Form.Group as={Row} controlId={name} className="mb-3">
<Form.Label className="col-4 col-form-label">{label}</Form.Label>
<div className="col-8">
<Form.Control
@ -50,16 +53,24 @@ const FormInput: React.FC<{
</div>
{help && <div className="form-text">{help}</div>}
</Form.Group>
}
);
};
export const ConfigModal: React.FC<{
show: boolean,
handleStartReconfigure: () => void,
handleConfigured: (config: RqbitDesktopConfig) => void,
handleCancel?: () => void,
initialConfig: RqbitDesktopConfig,
defaultConfig: RqbitDesktopConfig,
}> = ({ show, handleStartReconfigure, handleConfigured, handleCancel, initialConfig, defaultConfig }) => {
show: boolean;
handleStartReconfigure: () => void;
handleConfigured: (config: RqbitDesktopConfig) => void;
handleCancel?: () => void;
initialConfig: RqbitDesktopConfig;
defaultConfig: RqbitDesktopConfig;
}> = ({
show,
handleStartReconfigure,
handleConfigured,
handleCancel,
initialConfig,
defaultConfig,
}) => {
let [config, setConfig] = useState<RqbitDesktopConfig>(initialConfig);
let [loading, setLoading] = useState<boolean>(false);
@ -68,7 +79,7 @@ export const ConfigModal: React.FC<{
const handleInputChange = (e: any) => {
const name: string = e.target.name;
const value: any = e.target.value;
const [mainField, subField] = name.split('.', 2);
const [mainField, subField] = name.split(".", 2);
if (subField) {
setConfig((prevConfig: any) => ({
@ -88,7 +99,7 @@ export const ConfigModal: React.FC<{
const handleToggleChange = (e: any) => {
const name: string = e.target.name;
const [mainField, subField] = name.split('.', 2);
const [mainField, subField] = name.split(".", 2);
if (subField) {
setConfig((prevConfig: any) => ({
@ -122,21 +133,17 @@ export const ConfigModal: React.FC<{
details: e,
});
}
)
);
};
return (
<Modal show={show} size='xl' onHide={handleCancel}>
<Modal show={show} size="xl" onHide={handleCancel}>
<Modal.Header closeButton>
<Modal.Title>Configure Rqbit desktop</Modal.Title>
</Modal.Header>
<Modal.Body>
<ErrorComponent error={error}></ErrorComponent>
<Tabs
defaultActiveKey="home"
id="rqbit-config"
className="mb-3">
<Tabs defaultActiveKey="home" id="rqbit-config" className="mb-3">
<Tab className="mb-3" eventKey="home" title="Home">
<FormInput
label="Default download folder"
@ -219,7 +226,6 @@ export const ConfigModal: React.FC<{
/>
</Tab>
<Tab className="mb-3" eventKey="session_persistence" title="Session">
<legend>Session persistence</legend>
@ -293,16 +299,14 @@ export const ConfigModal: React.FC<{
help={`You'll access the API at http://${config.http_api.listen_addr}`}
/>
</Tab>
</Tabs>
</Modal.Body>
<Modal.Footer>
{!!handleCancel &&
{!!handleCancel && (
<Button variant="secondary" onClick={handleCancel}>
Cancel
</Button>
}
)}
<Button variant="secondary" onClick={() => setConfig(defaultConfig)}>
Reset to defaults
</Button>

View file

@ -1,5 +1,5 @@
import { StrictMode } from "react";
import ReactDOM from 'react-dom/client';
import ReactDOM from "react-dom/client";
import { APIContext } from "./rqbit-webui-src/rqbit-web";
import { API } from "./api";
import { invoke } from "@tauri-apps/api";
@ -18,12 +18,18 @@ async function get_current_config(): Promise<CurrentDesktopState> {
return invoke<CurrentDesktopState>("config_current");
}
Promise.all([get_version(), get_default_config(), get_current_config()]).then(([version, defaultConfig, currentState]) => {
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
Promise.all([get_version(), get_default_config(), get_current_config()]).then(
([version, defaultConfig, currentState]) => {
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<APIContext.Provider value={API}>
<RqbitDesktop version={version} defaultConfig={defaultConfig} currentState={currentState} />
<RqbitDesktop
version={version}
defaultConfig={defaultConfig}
currentState={currentState}
/>
</APIContext.Provider>
</StrictMode>
);
})
}
);

View file

@ -3,26 +3,33 @@ import { RqbitWebUI } from "./rqbit-webui-src/rqbit-web";
import { CurrentDesktopState, RqbitDesktopConfig } from "./configuration";
import { ConfigModal } from "./configure";
export const RqbitDesktop: React.FC<{
version: string,
defaultConfig: RqbitDesktopConfig,
currentState: CurrentDesktopState,
version: string;
defaultConfig: RqbitDesktopConfig;
currentState: CurrentDesktopState;
}> = ({ version, defaultConfig, currentState }) => {
let [configured, setConfigured] = useState<boolean>(currentState.configured);
let [config, setConfig] = useState<RqbitDesktopConfig>(currentState.config ?? defaultConfig);
let [config, setConfig] = useState<RqbitDesktopConfig>(
currentState.config ?? defaultConfig
);
let [configurationOpened, setConfigurationOpened] = useState<boolean>(false);
return <>
{configured && <RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>}
{configured && <a
return (
<>
{configured && (
<RqbitWebUI title={`Rqbit Desktop v${version}`}></RqbitWebUI>
)}
{configured && (
<a
className="bi bi-sliders2 position-absolute top-0 start-0 p-3 text-primary"
onClick={(e) => {
e.stopPropagation();
setConfigurationOpened(true);
}}
href="#"
aria-label="Settings" />}
aria-label="Settings"
/>
)}
<ConfigModal
show={!configured || configurationOpened}
handleStartReconfigure={() => {
@ -40,4 +47,5 @@ export const RqbitDesktop: React.FC<{
defaultConfig={defaultConfig}
/>
</>
}
);
};