1/n Use zustand to reduce re-renders
This commit is contained in:
parent
3dc2e3eace
commit
e6ef3ff23f
14 changed files with 210 additions and 87 deletions
34
crates/librqbit/webui/dist/assets/index.js
vendored
34
crates/librqbit/webui/dist/assets/index.js
vendored
File diff suppressed because one or more lines are too long
2
crates/librqbit/webui/dist/manifest.json
vendored
2
crates/librqbit/webui/dist/manifest.json
vendored
|
|
@ -11,7 +11,7 @@
|
|||
"css": [
|
||||
"assets/index-d46108e9.css"
|
||||
],
|
||||
"file": "assets/index-c93d50ee.js",
|
||||
"file": "assets/index-3e8911ff.js",
|
||||
"isEntry": true,
|
||||
"src": "index.html"
|
||||
}
|
||||
|
|
|
|||
41
crates/librqbit/webui/node_modules/.package-lock.json
generated
vendored
41
crates/librqbit/webui/node_modules/.package-lock.json
generated
vendored
|
|
@ -836,13 +836,13 @@
|
|||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz",
|
||||
"integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
|
@ -862,7 +862,7 @@
|
|||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
|
|
@ -2485,6 +2485,14 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
@ -2588,6 +2596,33 @@
|
|||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
|
||||
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
crates/librqbit/webui/package-lock.json
generated
44
crates/librqbit/webui/package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
|||
"lodash.debounce": "^4.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.12.0"
|
||||
"react-icons": "^4.12.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
|
|
@ -1195,13 +1196,13 @@
|
|||
"version": "15.7.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz",
|
||||
"integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
|
@ -1221,7 +1222,7 @@
|
|||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
|
|
@ -2844,6 +2845,14 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
@ -2947,6 +2956,33 @@
|
|||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
|
||||
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
"lodash.debounce": "^4.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.12.0"
|
||||
"react-icons": "^4.12.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import { useContext } from "react";
|
||||
import { TorrentId, ErrorDetails as ApiErrorDetails } from "../api-types";
|
||||
import { AppContext } from "../context";
|
||||
import { TorrentsList } from "./TorrentsList";
|
||||
import { ErrorComponent } from "./ErrorComponent";
|
||||
import { useTorrentStore } from "../stores/torrentStore";
|
||||
import { useErrorStore } from "../stores/errorStore";
|
||||
|
||||
export const RootContent = (props: {}) => {
|
||||
let closeableError = useErrorStore((state) => state.closeableError);
|
||||
let setCloseableError = useErrorStore((state) => state.setCloseableError);
|
||||
let otherError = useErrorStore((state) => state.otherError);
|
||||
let torrents = useTorrentStore((state) => state.torrents);
|
||||
let torrentsLoading = useTorrentStore((state) => state.torrentsLoading);
|
||||
|
||||
export const RootContent = (props: {
|
||||
closeableError: ApiErrorDetails | null;
|
||||
otherError: ApiErrorDetails | null;
|
||||
torrents: Array<TorrentId> | null;
|
||||
torrentsLoading: boolean;
|
||||
}) => {
|
||||
let ctx = useContext(AppContext);
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<ErrorComponent
|
||||
error={props.closeableError}
|
||||
remove={() => ctx.setCloseableError(null)}
|
||||
error={closeableError}
|
||||
remove={() => setCloseableError(null)}
|
||||
/>
|
||||
<ErrorComponent error={props.otherError} />
|
||||
<TorrentsList torrents={props.torrents} loading={props.torrentsLoading} />
|
||||
<ErrorComponent error={otherError} />
|
||||
<TorrentsList torrents={torrents} loading={torrentsLoading} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { TorrentStats } from "../../api-types";
|
||||
import {
|
||||
AppContext,
|
||||
APIContext,
|
||||
RefreshTorrentStatsContext,
|
||||
} from "../../context";
|
||||
import { APIContext, RefreshTorrentStatsContext } from "../../context";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { DeleteTorrentModal } from "../modal/DeleteTorrentModal";
|
||||
import { FaPause, FaPlay, FaTrash } from "react-icons/fa";
|
||||
import { useErrorStore } from "../../stores/errorStore";
|
||||
|
||||
export const TorrentActions: React.FC<{
|
||||
id: number;
|
||||
|
|
@ -23,7 +20,8 @@ export const TorrentActions: React.FC<{
|
|||
const canPause = state == "live";
|
||||
const canUnpause = state == "paused" || state == "error";
|
||||
|
||||
const ctx = useContext(AppContext);
|
||||
const setCloseableError = useErrorStore((state) => state.setCloseableError);
|
||||
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const unpause = () => {
|
||||
|
|
@ -34,7 +32,7 @@ export const TorrentActions: React.FC<{
|
|||
refreshCtx.refresh();
|
||||
},
|
||||
(e) => {
|
||||
ctx.setCloseableError({
|
||||
setCloseableError({
|
||||
text: `Error starting torrent id=${id}`,
|
||||
details: e,
|
||||
});
|
||||
|
|
@ -51,7 +49,7 @@ export const TorrentActions: React.FC<{
|
|||
refreshCtx.refresh();
|
||||
},
|
||||
(e) => {
|
||||
ctx.setCloseableError({
|
||||
setCloseableError({
|
||||
text: `Error pausing torrent id=${id}`,
|
||||
details: e,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { AppContext, APIContext } from "../../context";
|
||||
import { APIContext } from "../../context";
|
||||
import { ErrorWithLabel } from "../../rqbit-web";
|
||||
import { ErrorComponent } from "../ErrorComponent";
|
||||
import { Spinner } from "../Spinner";
|
||||
|
|
@ -7,6 +7,7 @@ import { Modal } from "./Modal";
|
|||
import { ModalBody } from "./ModalBody";
|
||||
import { ModalFooter } from "./ModalFooter";
|
||||
import { Button } from "../buttons/Button";
|
||||
import { useTorrentStore } from "../../stores/torrentStore";
|
||||
|
||||
export const DeleteTorrentModal: React.FC<{
|
||||
id: number;
|
||||
|
|
@ -20,8 +21,8 @@ export const DeleteTorrentModal: React.FC<{
|
|||
const [error, setError] = useState<ErrorWithLabel | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const ctx = useContext(AppContext);
|
||||
const API = useContext(APIContext);
|
||||
const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);
|
||||
|
||||
const close = () => {
|
||||
setDeleteFiles(false);
|
||||
|
|
@ -37,7 +38,7 @@ export const DeleteTorrentModal: React.FC<{
|
|||
|
||||
call(id)
|
||||
.then(() => {
|
||||
ctx.refreshTorrents();
|
||||
refreshTorrents();
|
||||
close();
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { AddTorrentResponse, AddTorrentOptions } from "../../api-types";
|
||||
import { AppContext, APIContext } from "../../context";
|
||||
import { APIContext } from "../../context";
|
||||
import { ErrorComponent } from "../ErrorComponent";
|
||||
import { formatBytes } from "../../helper/formatBytes";
|
||||
import { ErrorWithLabel } from "../../rqbit-web";
|
||||
|
|
@ -14,6 +14,7 @@ import { Fieldset } from "../forms/Fieldset";
|
|||
import { FormInput } from "../forms/FormInput";
|
||||
import { Form } from "../forms/Form";
|
||||
import { FileListInput } from "../FileListInput";
|
||||
import { useTorrentStore } from "../../stores/torrentStore";
|
||||
|
||||
export const FileSelectionModal = (props: {
|
||||
onHide: () => void;
|
||||
|
|
@ -35,7 +36,7 @@ export const FileSelectionModal = (props: {
|
|||
const [uploadError, setUploadError] = useState<ErrorWithLabel | null>(null);
|
||||
const [unpopularTorrent, setUnpopularTorrent] = useState(false);
|
||||
const [outputFolder, setOutputFolder] = useState<string>("");
|
||||
const ctx = useContext(AppContext);
|
||||
const refreshTorrents = useTorrentStore((state) => state.refreshTorrents);
|
||||
const API = useContext(APIContext);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -77,7 +78,7 @@ export const FileSelectionModal = (props: {
|
|||
.then(
|
||||
() => {
|
||||
onHide();
|
||||
ctx.refreshTorrents();
|
||||
refreshTorrents();
|
||||
},
|
||||
(e) => {
|
||||
setUploadError({ text: "Error starting torrent", details: e });
|
||||
|
|
|
|||
|
|
@ -31,8 +31,4 @@ export const APIContext = createContext<RqbitAPI>({
|
|||
return null;
|
||||
},
|
||||
});
|
||||
export const AppContext = createContext<ContextType>({
|
||||
setCloseableError: (_) => {},
|
||||
refreshTorrents: () => {},
|
||||
});
|
||||
export const RefreshTorrentStatsContext = createContext({ refresh: () => {} });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { TorrentId, ErrorDetails as ApiErrorDetails } from "./api-types";
|
||||
import { AppContext, APIContext } from "./context";
|
||||
import { ErrorDetails as ApiErrorDetails } from "./api-types";
|
||||
import { APIContext } from "./context";
|
||||
import { RootContent } from "./components/RootContent";
|
||||
import { customSetInterval } from "./helper/customSetInterval";
|
||||
import { IconButton } from "./components/buttons/IconButton";
|
||||
|
|
@ -8,6 +8,8 @@ import { BsBodyText, BsMoon } from "react-icons/bs";
|
|||
import { LogStreamModal } from "./components/modal/LogStreamModal";
|
||||
import { Header } from "./components/Header";
|
||||
import { DarkMode } from "./helper/darkMode";
|
||||
import { useTorrentStore } from "./stores/torrentStore";
|
||||
import { useErrorStore } from "./stores/errorStore";
|
||||
|
||||
export interface ErrorWithLabel {
|
||||
text: string;
|
||||
|
|
@ -23,17 +25,20 @@ export const RqbitWebUI = (props: {
|
|||
title: string;
|
||||
menuButtons?: JSX.Element[];
|
||||
}) => {
|
||||
const [closeableError, setCloseableError] = useState<ErrorWithLabel | null>(
|
||||
null
|
||||
);
|
||||
const [otherError, setOtherError] = useState<ErrorWithLabel | null>(null);
|
||||
|
||||
const [torrents, setTorrents] = useState<Array<TorrentId> | null>(null);
|
||||
const [torrentsLoading, setTorrentsLoading] = useState(false);
|
||||
let [logsOpened, setLogsOpened] = useState<boolean>(false);
|
||||
const setCloseableError = useErrorStore((state) => state.setCloseableError);
|
||||
const setOtherError = useErrorStore((state) => state.setOtherError);
|
||||
|
||||
const API = useContext(APIContext);
|
||||
|
||||
const setTorrents = useTorrentStore((state) => state.setTorrents);
|
||||
const setTorrentsLoading = useTorrentStore(
|
||||
(state) => state.setTorrentsLoading
|
||||
);
|
||||
const setRefreshTorrents = useTorrentStore(
|
||||
(state) => state.setRefreshTorrents
|
||||
);
|
||||
|
||||
const refreshTorrents = async () => {
|
||||
setTorrentsLoading(true);
|
||||
let torrents = await API.listTorrents().finally(() =>
|
||||
|
|
@ -41,6 +46,7 @@ export const RqbitWebUI = (props: {
|
|||
);
|
||||
setTorrents(torrents.torrents);
|
||||
};
|
||||
setRefreshTorrents(refreshTorrents);
|
||||
|
||||
useEffect(() => {
|
||||
return customSetInterval(
|
||||
|
|
@ -66,35 +72,25 @@ export const RqbitWebUI = (props: {
|
|||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={context}>
|
||||
<div className="dark:bg-gray-900 dark:text-gray-200 min-h-screen">
|
||||
<Header title={props.title} />
|
||||
<div className="relative">
|
||||
{/* Menu buttons */}
|
||||
<div className="absolute top-0 start-0 pl-2 z-10">
|
||||
{props.menuButtons &&
|
||||
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
|
||||
<IconButton onClick={() => setLogsOpened(true)}>
|
||||
<BsBodyText />
|
||||
</IconButton>
|
||||
<IconButton onClick={DarkMode.toggle}>
|
||||
<BsMoon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<RootContent
|
||||
closeableError={closeableError}
|
||||
otherError={otherError}
|
||||
torrents={torrents}
|
||||
torrentsLoading={torrentsLoading}
|
||||
/>
|
||||
<div className="dark:bg-gray-900 dark:text-gray-200 min-h-screen">
|
||||
<Header title={props.title} />
|
||||
<div className="relative">
|
||||
{/* Menu buttons */}
|
||||
<div className="absolute top-0 start-0 pl-2 z-10">
|
||||
{props.menuButtons &&
|
||||
props.menuButtons.map((b, i) => <span key={i}>{b}</span>)}
|
||||
<IconButton onClick={() => setLogsOpened(true)}>
|
||||
<BsBodyText />
|
||||
</IconButton>
|
||||
<IconButton onClick={DarkMode.toggle}>
|
||||
<BsMoon />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<LogStreamModal
|
||||
show={logsOpened}
|
||||
onClose={() => setLogsOpened(false)}
|
||||
/>
|
||||
<RootContent />
|
||||
</div>
|
||||
</AppContext.Provider>
|
||||
|
||||
<LogStreamModal show={logsOpened} onClose={() => setLogsOpened(false)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
21
crates/librqbit/webui/src/stores/errorStore.ts
Normal file
21
crates/librqbit/webui/src/stores/errorStore.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { create } from "zustand";
|
||||
import { ErrorDetails } from "../api-types";
|
||||
|
||||
export interface ErrorWithLabel {
|
||||
text: string;
|
||||
details?: ErrorDetails;
|
||||
}
|
||||
|
||||
export const useErrorStore = create<{
|
||||
closeableError: ErrorWithLabel | null;
|
||||
setCloseableError: (error: ErrorWithLabel | null) => void;
|
||||
|
||||
otherError: ErrorWithLabel | null;
|
||||
setOtherError: (error: ErrorWithLabel | null) => void;
|
||||
}>((set) => ({
|
||||
closeableError: null,
|
||||
setCloseableError: (closeableError) => set(() => ({ closeableError })),
|
||||
|
||||
otherError: null,
|
||||
setOtherError: (otherError) => set(() => ({ otherError })),
|
||||
}));
|
||||
22
crates/librqbit/webui/src/stores/torrentStore.ts
Normal file
22
crates/librqbit/webui/src/stores/torrentStore.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { create } from "zustand";
|
||||
import { TorrentId } from "../api-types";
|
||||
|
||||
export interface TorrentStore {
|
||||
torrents: Array<TorrentId> | null;
|
||||
setTorrents: (torrents: Array<TorrentId>) => void;
|
||||
|
||||
torrentsLoading: boolean;
|
||||
setTorrentsLoading: (loading: boolean) => void;
|
||||
|
||||
refreshTorrents: () => void;
|
||||
setRefreshTorrents: (callback: () => void) => void;
|
||||
}
|
||||
|
||||
export const useTorrentStore = create<TorrentStore>((set) => ({
|
||||
torrents: null,
|
||||
torrentsLoading: false,
|
||||
setTorrentsLoading: (loading: boolean) => set({ torrentsLoading: loading }),
|
||||
setTorrents: (torrents) => set({ torrents }),
|
||||
refreshTorrents: () => {},
|
||||
setRefreshTorrents: (callback) => set({ refreshTorrents: callback }),
|
||||
}));
|
||||
3
desktop/package-lock.json
generated
3
desktop/package-lock.json
generated
|
|
@ -34,7 +34,8 @@
|
|||
"lodash.debounce": "^4.0.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^4.12.0"
|
||||
"react-icons": "^4.12.0",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue