diff --git a/src/tribler/ui/public/locales/en_US.json b/src/tribler/ui/public/locales/en_US.json index 8b844a2a24..ddbbfaa480 100644 --- a/src/tribler/ui/public/locales/en_US.json +++ b/src/tribler/ui/public/locales/en_US.json @@ -134,5 +134,30 @@ "ErrorNotification": "Tribler encountered an error! What do you want to do?", "ErrorSearchButton": "Search for solutions", "ErrorReportButton": "Report on GitHub", - "ErrorIgnoreButton": "Ignore" + "ErrorIgnoreButton": "Ignore", + "ToastErrorGenNetworkErr": "Connection error.", + "ToastErrorSetLanguage": "Failed set language!", + "ToastErrorShutdown": "Failed to send shutdown signal!", + "ToastErrorDefaultDLDir": "Failed to retrieve default download directory!", + "ToastErrorDirectoryAdd": "Failed to add directory!", + "ToastErrorCreateTorrent": "Failed to create torrent {{name}}!", + "ToastErrorGetSettings": "Failed retrieve settings!", + "ToastErrorSetSettings": "Failed save settings!", + "ToastErrorGetMetainfo": "Failed to retrieve metadata!", + "ToastErrorBrowseFiles": "Failed to open path!", + "ToastErrorEnableHealth": "Failed enable health measurements!", + "ToastErrorDisableHealth": "Failed disable health measurements!", + "ToastErrorEnableAsyncio": "Failed enable asyncio debugger!", + "ToastErrorDisableAsyncio": "Failed disable asyncio debugger!", + "ToastErrorSlowness": "Failed change slowness threshold!", + "ToastErrorDownloadStart": "Failed to start download!", + "ToastErrorDownloadPlay": "Failed to play download!", + "ToastErrorDownloadStop": "Failed to stop download!", + "ToastErrorDownloadRemove": "Failed to remove download!", + "ToastErrorDownloadCheck": "Failed to check download!", + "ToastErrorDownloadMove": "Failed to move download!", + "ToastErrorDownloadSetHops": "Failed change the anonymity of download!", + "ToastErrorDownloadSetFiles": "Failed to set files for download!", + "ToastErrorUpgradeFailed": "Upgrade failed!", + "ToastErrorRemoveVersion": "Failed to remove version {{version}}!" } diff --git a/src/tribler/ui/public/locales/es_ES.json b/src/tribler/ui/public/locales/es_ES.json index 4f03ddbe3c..ecf1f753f9 100644 --- a/src/tribler/ui/public/locales/es_ES.json +++ b/src/tribler/ui/public/locales/es_ES.json @@ -134,5 +134,30 @@ "ErrorNotification": "¡Tribler encontró un error! ", "ErrorSearchButton": "Buscar soluciones", "ErrorReportButton": "Informe en GitHub", - "ErrorIgnoreButton": "Ignorar" + "ErrorIgnoreButton": "Ignorar", + "ToastErrorGenNetworkErr": "Error de conexión.", + "ToastErrorSetLanguage": "¡Error al establecer el idioma!", + "ToastErrorShutdown": "¡No se pudo enviar la señal de apagado!", + "ToastErrorDefaultDLDir": "¡No se pudo recuperar el directorio de descarga predeterminado!", + "ToastErrorDirectoryAdd": "¡No se pudo agregar el directorio!", + "ToastErrorCreateTorrent": "No se pudo crear torrent {{name}}!", + "ToastErrorGetSettings": "¡Error al recuperar la configuración!", + "ToastErrorSetSettings": "¡Error al guardar la configuración!", + "ToastErrorGetMetainfo": "¡No se pudieron recuperar los metadatos!", + "ToastErrorBrowseFiles": "¡No se pudo abrir el camino!", + "ToastErrorEnableHealth": "¡Error al habilitar las mediciones de salud!", + "ToastErrorDisableHealth": "¡Error al desactivar las mediciones de salud!", + "ToastErrorEnableAsyncio": "¡Error al habilitar el depurador asyncio!", + "ToastErrorDisableAsyncio": "¡Error al desactivar el depurador asyncio!", + "ToastErrorSlowness": "¡Umbral de lentitud de cambio fallido!", + "ToastErrorDownloadStart": "¡No se pudo iniciar la descarga!", + "ToastErrorDownloadPlay": "¡No se pudo reproducir la descarga!", + "ToastErrorDownloadStop": "¡No se pudo detener la descarga!", + "ToastErrorDownloadRemove": "¡No se pudo eliminar la descarga!", + "ToastErrorDownloadCheck": "¡No se pudo verificar la descarga!", + "ToastErrorDownloadMove": "¡No se pudo mover la descarga!", + "ToastErrorDownloadSetHops": "¡Error al cambiar el anonimato de la descarga!", + "ToastErrorDownloadSetFiles": "¡No se pudieron configurar los archivos para descargar!", + "ToastErrorUpgradeFailed": "¡La actualización falló!", + "ToastErrorRemoveVersion": "No se pudo eliminar la versión {{version}}!" } diff --git a/src/tribler/ui/public/locales/pt_BR.json b/src/tribler/ui/public/locales/pt_BR.json index b532fd15de..0e34012aed 100644 --- a/src/tribler/ui/public/locales/pt_BR.json +++ b/src/tribler/ui/public/locales/pt_BR.json @@ -126,5 +126,30 @@ "ErrorNotification": "Tribler encontrou um erro! ", "ErrorSearchButton": "Procure soluções", "ErrorReportButton": "Relatório no GitHub", - "ErrorIgnoreButton": "Ignorar" + "ErrorIgnoreButton": "Ignorar", + "ToastErrorGenNetworkErr": "Erro de conexão.", + "ToastErrorSetLanguage": "Falha ao definir idioma!", + "ToastErrorShutdown": "Falha ao enviar sinal de desligamento!", + "ToastErrorDefaultDLDir": "Falha ao recuperar o diretório de download padrão!", + "ToastErrorDirectoryAdd": "Falha ao adicionar diretório!", + "ToastErrorCreateTorrent": "Falha ao criar torrent {{name}}!", + "ToastErrorGetSettings": "Falha ao recuperar configurações!", + "ToastErrorSetSettings": "Falha ao salvar configurações!", + "ToastErrorGetMetainfo": "Falha ao recuperar metadados!", + "ToastErrorBrowseFiles": "Falha ao abrir caminho!", + "ToastErrorEnableHealth": "Falha ao ativar medições de integridade!", + "ToastErrorDisableHealth": "Falha ao desativar medições de integridade!", + "ToastErrorEnableAsyncio": "Falha ao ativar o depurador assíncrono!", + "ToastErrorDisableAsyncio": "Falha ao desativar o depurador assíncrono!", + "ToastErrorSlowness": "Falha ao alterar limite de lentidão!", + "ToastErrorDownloadStart": "Falha ao iniciar o download!", + "ToastErrorDownloadPlay": "Falha ao reproduzir o download!", + "ToastErrorDownloadStop": "Falha ao interromper o download!", + "ToastErrorDownloadRemove": "Falha ao remover o download!", + "ToastErrorDownloadCheck": "Falha ao verificar o download!", + "ToastErrorDownloadMove": "Falha ao mover o download!", + "ToastErrorDownloadSetHops": "Falha ao alterar o anonimato do download!", + "ToastErrorDownloadSetFiles": "Falha ao definir arquivos para download!", + "ToastErrorUpgradeFailed": "Falha na atualização!", + "ToastErrorRemoveVersion": "Falha ao remover a versão {{version}}!" } diff --git a/src/tribler/ui/public/locales/ru_RU.json b/src/tribler/ui/public/locales/ru_RU.json index bb7c40db62..dc96084b7a 100644 --- a/src/tribler/ui/public/locales/ru_RU.json +++ b/src/tribler/ui/public/locales/ru_RU.json @@ -134,5 +134,30 @@ "ErrorNotification": "Триблер обнаружил ошибку! ", "ErrorSearchButton": "Поиск решений", "ErrorReportButton": "Отчет на GitHub", - "ErrorIgnoreButton": "игнорировать" + "ErrorIgnoreButton": "игнорировать", + "ToastErrorGenNetworkErr": "Ошибка подключения.", + "ToastErrorSetLanguage": "Не удалось установить язык!", + "ToastErrorShutdown": "Не удалось отправить сигнал выключения!", + "ToastErrorDefaultDLDir": "Не удалось получить каталог загрузки по умолчанию!", + "ToastErrorDirectoryAdd": "Не удалось добавить каталог!", + "ToastErrorCreateTorrent": "Не удалось создать торрент {{name}}!", + "ToastErrorGetSettings": "Не удалось получить настройки!", + "ToastErrorSetSettings": "Не удалось сохранить настройки!", + "ToastErrorGetMetainfo": "Не удалось получить метаданные!", + "ToastErrorBrowseFiles": "Не удалось открыть путь!", + "ToastErrorEnableHealth": "Не удалось включить измерения состояния здоровья!", + "ToastErrorDisableHealth": "Не удалось отключить измерения здоровья!", + "ToastErrorEnableAsyncio": "Не удалось включить асинхронный отладчик!", + "ToastErrorDisableAsyncio": "Не удалось отключить асинхронный отладчик!", + "ToastErrorSlowness": "Не удалось изменить порог медленности!", + "ToastErrorDownloadStart": "Не удалось начать загрузку!", + "ToastErrorDownloadPlay": "Не удалось загрузить игру!", + "ToastErrorDownloadStop": "Не удалось остановить загрузку!", + "ToastErrorDownloadRemove": "Не удалось удалить загрузку!", + "ToastErrorDownloadCheck": "Не удалось проверить загрузку!", + "ToastErrorDownloadMove": "Не удалось переместить загрузку!", + "ToastErrorDownloadSetHops": "Не удалось изменить анонимность загрузки!", + "ToastErrorDownloadSetFiles": "Не удалось установить файлы для скачивания!", + "ToastErrorUpgradeFailed": "Обновление не удалось!", + "ToastErrorRemoveVersion": "Не удалось удалить версию {{version}}!" } diff --git a/src/tribler/ui/public/locales/zh_CN.json b/src/tribler/ui/public/locales/zh_CN.json index 15d0fd9937..c57c078f78 100644 --- a/src/tribler/ui/public/locales/zh_CN.json +++ b/src/tribler/ui/public/locales/zh_CN.json @@ -133,5 +133,30 @@ "ErrorNotification": "Tribler 遇到错误!", "ErrorSearchButton": "搜索解决方案", "ErrorReportButton": "在 GitHub 上报告", - "ErrorIgnoreButton": "忽略" + "ErrorIgnoreButton": "忽略", + "ToastErrorGenNetworkErr": "连接错误。", + "ToastErrorSetLanguage": "设置语言失败!", + "ToastErrorShutdown": "发送关机信号失败!", + "ToastErrorDefaultDLDir": "检索默认下载目录失败!", + "ToastErrorDirectoryAdd": "添加目录失败!", + "ToastErrorCreateTorrent": "创建 torrent 失败 {{name}}!", + "ToastErrorGetSettings": "检索设置失败!", + "ToastErrorSetSettings": "保存设置失败!", + "ToastErrorGetMetainfo": "检索元数据失败!", + "ToastErrorBrowseFiles": "打开路径失败!", + "ToastErrorEnableHealth": "启用健康测量失败!", + "ToastErrorDisableHealth": "禁用运行状况测量失败!", + "ToastErrorEnableAsyncio": "启用异步调试器失败!", + "ToastErrorDisableAsyncio": "禁用异步调试器失败!", + "ToastErrorSlowness": "更改缓慢阈值失败!", + "ToastErrorDownloadStart": "无法开始下载!", + "ToastErrorDownloadPlay": "下载播放失败!", + "ToastErrorDownloadStop": "无法停止下载!", + "ToastErrorDownloadRemove": "删除下载失败!", + "ToastErrorDownloadCheck": "检查下载失败!", + "ToastErrorDownloadMove": "移动下载失败!", + "ToastErrorDownloadSetHops": "更改下载匿名失败!", + "ToastErrorDownloadSetFiles": "设置文件下载失败!", + "ToastErrorUpgradeFailed": "升级失败!", + "ToastErrorRemoveVersion": "删除版本失败 {{version}}!" } diff --git a/src/tribler/ui/src/components/add-torrent.tsx b/src/tribler/ui/src/components/add-torrent.tsx index 081a83546b..2adb8c344c 100644 --- a/src/tribler/ui/src/components/add-torrent.tsx +++ b/src/tribler/ui/src/components/add-torrent.tsx @@ -1,10 +1,12 @@ import { useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; +import toast from 'react-hot-toast'; import { Button } from "./ui/button"; import { PlusIcon, Cloud, File as FileIcon } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { Input } from "./ui/input"; import SaveAs from "@/dialogs/SaveAs"; import CreateTorrent from "@/dialogs/CreateTorrent"; @@ -130,7 +132,14 @@ export function AddTorrent() { } else { for (let file of files) { - (async () => { await triblerService.startDownloadFromFile(file) })(); + (async () => { + const response = await triblerService.startDownloadFromFile(file); + if (response === undefined) { + toast.error(`${t("ToastErrorStartDownload")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorStartDownload")} ${response.error}`); + } + })(); } } navigate("/downloads/all"); diff --git a/src/tribler/ui/src/components/language-select.tsx b/src/tribler/ui/src/components/language-select.tsx index 19fcef265b..49bc3a91a6 100644 --- a/src/tribler/ui/src/components/language-select.tsx +++ b/src/tribler/ui/src/components/language-select.tsx @@ -3,7 +3,9 @@ import { Button } from "./ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; import { useTranslation } from "react-i18next"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useEffect } from "react"; +import toast from 'react-hot-toast'; const LanguageSelect = () => { @@ -19,9 +21,12 @@ const LanguageSelect = () => { const changeLanguage = async (lng: string) => { setLanguage(lng); i18n.changeLanguage(lng); - await triblerService.setSettings({ - ui: { lang: lng } - }); + const response = await triblerService.setSettings({ ui: { lang: lng } }); + if (response === undefined) { + toast.error(`${t("ToastErrorSetLanguage")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetLanguage")} ${response.error}`); + } }; return ( diff --git a/src/tribler/ui/src/components/layouts/Header.tsx b/src/tribler/ui/src/components/layouts/Header.tsx index c03f4c8229..120cb4d803 100644 --- a/src/tribler/ui/src/components/layouts/Header.tsx +++ b/src/tribler/ui/src/components/layouts/Header.tsx @@ -7,6 +7,7 @@ import { ModeToggle } from "../mode-toggle"; import { Search } from "./Search"; import LanguageSelect from "../language-select"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useInterval } from "@/hooks/useInterval"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"; import { useEffect, useRef, useState } from "react"; @@ -133,7 +134,15 @@ export function Header() { diff --git a/src/tribler/ui/src/components/layouts/Search.tsx b/src/tribler/ui/src/components/layouts/Search.tsx index 32be765d4c..fab8e50ece 100644 --- a/src/tribler/ui/src/components/layouts/Search.tsx +++ b/src/tribler/ui/src/components/layouts/Search.tsx @@ -1,5 +1,6 @@ 'use client'; import { triblerService } from '@/services/tribler.service'; +import { isErrorDict } from "@/services/reporting"; import { Autocomplete } from '../ui/autocomplete'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -12,7 +13,12 @@ export function Search() { { - return await triblerService.getCompletions(value); + const response = await triblerService.getCompletions(value); + if (response === undefined || isErrorDict(response)) { + return []; + } else { + return response; + } }} onChange={(query) => navigate('/search?query=' + query)} /> diff --git a/src/tribler/ui/src/components/swarm-health.tsx b/src/tribler/ui/src/components/swarm-health.tsx index d19a8df1cb..ddcba4a043 100644 --- a/src/tribler/ui/src/components/swarm-health.tsx +++ b/src/tribler/ui/src/components/swarm-health.tsx @@ -1,11 +1,15 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import { Torrent } from "@/models/torrent.model"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; import { formatTimeAgo } from "@/lib/utils"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; export function SwarmHealth({ torrent }: { torrent: Torrent }) { + const { t } = useTranslation(); const [checking, setChecking] = useState(false) useEffect(() => { @@ -26,8 +30,16 @@ export function SwarmHealth({ torrent }: { torrent: Torrent }) {
{ - triblerService.getTorrentHealth(torrent.infohash) setChecking(true); + triblerService.getTorrentHealth(torrent.infohash).then((response) => { + if (response === undefined){ + setChecking(false); + toast.error(`${t("ToastErrorDownloadCheck")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)) { + setChecking(false); + toast.error(`${t("ToastErrorDownloadCheck")} ${response.error}`); + } + }); }} > {checking ? diff --git a/src/tribler/ui/src/dialogs/CreateTorrent.tsx b/src/tribler/ui/src/dialogs/CreateTorrent.tsx index 9d38ba94b1..32a0429035 100644 --- a/src/tribler/ui/src/dialogs/CreateTorrent.tsx +++ b/src/tribler/ui/src/dialogs/CreateTorrent.tsx @@ -1,5 +1,6 @@ import SimpleTable from "@/components/ui/simple-table"; import { useEffect, useState } from "react"; +import toast from 'react-hot-toast'; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { DialogProps } from "@radix-ui/react-dialog"; @@ -11,6 +12,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { Textarea } from "@/components/ui/textarea"; import { useTranslation } from "react-i18next"; import { triblerService } from "@/services/tribler.service"; +import { ErrorDict, isErrorDict } from "@/services/reporting"; import SelectRemotePath from "./SelectRemotePath"; import { PathInput } from "@/components/path-input"; import { Settings } from "@/models/settings.model"; @@ -52,15 +54,27 @@ export default function CreateTorrent(props: JSX.IntrinsicAttributes & DialogPro async function resetPath() { const settings = await triblerService.getSettings(); - setDestination(settings.libtorrent.download_defaults.saveas); + if (settings === undefined){ + toast.error(`${t("ToastErrorDefaultDLDir")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(settings)) { + toast.error(`${t("ToastErrorDefaultDLDir")} ${settings.error}`); + } else { + setDestination(settings.libtorrent.download_defaults.saveas); + } } async function addDir(dirname: string) { const response = await triblerService.listFiles(dirname, true); - setFiles([ - ...files, - ...response.paths.filter((item) => !item.dir) - ]); + if (response === undefined){ + toast.error(`${t("ToastErrorDirectoryAdd")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)) { + toast.error(`${t("ToastErrorDirectoryAdd")} ${response.error}`); + } else { + setFiles([ + ...files, + ...response.paths.filter((item) => !item.dir) + ]); + } } async function addFile(filename: string) { @@ -160,7 +174,16 @@ export default function CreateTorrent(props: JSX.IntrinsicAttributes & DialogPro variant="outline" type="submit" onClick={() => { - triblerService.createTorrent(name, description, files.map((f) => f.path), destination, seed); + triblerService.createTorrent(name, description, files.map((f) => f.path), destination, seed).then( + (response) => { + if (response === undefined) { + toast.error(`${t("ToastErrorCreateTorrent", {name: name})} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)) { + // Quinten: according to the typing, response could not be a ErrorDict here?! + toast.error(`${t("ToastErrorCreateTorrent", {name: name})} ${(response as ErrorDict).error}`); + } + } + ); if (props.onOpenChange) props.onOpenChange(false); }} diff --git a/src/tribler/ui/src/dialogs/SaveAs.tsx b/src/tribler/ui/src/dialogs/SaveAs.tsx index ad7bba55d2..31aa460b16 100644 --- a/src/tribler/ui/src/dialogs/SaveAs.tsx +++ b/src/tribler/ui/src/dialogs/SaveAs.tsx @@ -1,6 +1,8 @@ import SimpleTable from "@/components/ui/simple-table"; import { useEffect, useState } from "react"; +import toast from 'react-hot-toast'; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { formatBytes, getFilesFromMetainfo, getRowSelection, translateHeader } from "@/lib/utils"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; @@ -12,9 +14,19 @@ import { ColumnDef } from "@tanstack/react-table"; import { useNavigate } from "react-router-dom"; import { Settings } from "@/models/settings.model"; import { useTranslation } from "react-i18next"; +import { TFunction } from 'i18next'; import { PathInput } from "@/components/path-input"; +function startDownloadCallback(response: any, t: TFunction) { + // We have to receive a translation function. Otherwise, we violate React's hook scoping. + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadStart")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadStart")} ${response.error}`); + } +} + const fileColumns: ColumnDef[] = [ { id: "select", @@ -95,6 +107,13 @@ export default function SaveAs(props: SaveAsProps & JSX.IntrinsicAttributes & Di setExists(false); setFiles([]) const newSettings = await triblerService.getSettings(); + if (newSettings === undefined) { + setError(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + return; + } else if (isErrorDict(newSettings)){ + setError(`${t("ToastErrorGetSettings")} ${newSettings.error}`); + return; + } const safeSeeding = !!newSettings?.libtorrent?.download_defaults?.safeseeding_enabled; const safeDownloading = !!newSettings?.libtorrent?.download_defaults?.anonymity_enabled; setSettings(newSettings); @@ -114,8 +133,10 @@ export default function SaveAs(props: SaveAsProps & JSX.IntrinsicAttributes & Di response = await triblerService.getMetainfo(uri); } - if (response?.error) { - setError(response.error); + if (response === undefined) { + setError(`${t("ToastErrorGetMetainfo")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)) { + setError(`t("ToastErrorGetMetainfo")} ${response.error}`); } else if (response) { setFiles(getFilesFromMetainfo(response.metainfo)); setExists(!!response.download_exists); @@ -145,10 +166,10 @@ export default function SaveAs(props: SaveAsProps & JSX.IntrinsicAttributes & Di if (!settings) return; if (torrent) { - triblerService.startDownloadFromFile(torrent, params); + triblerService.startDownloadFromFile(torrent, params).then((response) => {startDownloadCallback(response, t)}); } else if (uri) { - triblerService.startDownload(uri, params); + triblerService.startDownload(uri, params).then((response) => {startDownloadCallback(response, t)}); } if (props.onOpenChange) { diff --git a/src/tribler/ui/src/dialogs/SelectRemotePath.tsx b/src/tribler/ui/src/dialogs/SelectRemotePath.tsx index f760cf002d..3fb39aca85 100644 --- a/src/tribler/ui/src/dialogs/SelectRemotePath.tsx +++ b/src/tribler/ui/src/dialogs/SelectRemotePath.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from "react"; +import toast from 'react-hot-toast'; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { DialogProps } from "@radix-ui/react-dialog"; @@ -37,9 +39,15 @@ export default function SelectRemotePath(props: SelectRemotePathProps & JSX.Intr async function reloadPaths(dir: string) { const response = await triblerService.browseFiles(dir, showFiles || false); - setPaths(response.paths); - setCurrentPath(response.current); - setLastClicked((selectDir) ? { name: '', path: response.current, dir: true } : undefined); + if (response === undefined) { + toast.error(`${t("ToastErrorBrowseFiles")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorBrowseFiles")} ${response.error}`); + } else { + setPaths(response.paths); + setCurrentPath(response.current); + setLastClicked((selectDir) ? { name: '', path: response.current, dir: true } : undefined); + } } return ( diff --git a/src/tribler/ui/src/models/bucket.model.tsx b/src/tribler/ui/src/models/bucket.model.tsx index 274d97fefd..184a38cbea 100644 --- a/src/tribler/ui/src/models/bucket.model.tsx +++ b/src/tribler/ui/src/models/bucket.model.tsx @@ -18,3 +18,34 @@ export interface Bucket { endpoint: endpoint; peers: DHTPeer[]; } + +export interface Endpoint { + endpoint: string; + node_id: string; + routing_table_size: number; + routing_table_buckets: number; + num_keys_in_store: number; +} + +export interface DHTStats { + peer_id: string; + num_tokens: number; + endpoints: Endpoint[]; + num_peers_in_store?: Map; + num_store_for_me?: Map; +} + +export interface Values { + values: { + public_key: string | null; + key: string; + value: string; + }[]; + debug: { + requests: number; + responses: number; + responses_with_nodes: number; + responses_with_values: number; + time: number; + }; +} \ No newline at end of file diff --git a/src/tribler/ui/src/models/drift.model.tsx b/src/tribler/ui/src/models/drift.model.tsx new file mode 100644 index 0000000000..3c0c0e07c2 --- /dev/null +++ b/src/tribler/ui/src/models/drift.model.tsx @@ -0,0 +1,4 @@ +export interface Drift { + timestamp: number; + drift: number; +} diff --git a/src/tribler/ui/src/models/metainfo.tsx b/src/tribler/ui/src/models/metainfo.tsx index b5103052da..3f757cfd47 100644 --- a/src/tribler/ui/src/models/metainfo.tsx +++ b/src/tribler/ui/src/models/metainfo.tsx @@ -7,7 +7,7 @@ export interface Metainfo { } } -interface MetainfoFile { +export interface MetainfoFile { length: number; path: string[]; diff --git a/src/tribler/ui/src/models/task.model.tsx b/src/tribler/ui/src/models/task.model.tsx new file mode 100644 index 0000000000..e4ba2777f8 --- /dev/null +++ b/src/tribler/ui/src/models/task.model.tsx @@ -0,0 +1,8 @@ +export interface Task { + name: string; + running: boolean; + stack: string[]; + taskmanager?: string; + start_time?: number; + interval?: number; +} diff --git a/src/tribler/ui/src/pages/Debug/Asyncio/Health.tsx b/src/tribler/ui/src/pages/Debug/Asyncio/Health.tsx index 8297ef3ba9..7ec2056c3d 100644 --- a/src/tribler/ui/src/pages/Debug/Asyncio/Health.tsx +++ b/src/tribler/ui/src/pages/Debug/Asyncio/Health.tsx @@ -1,31 +1,48 @@ import { useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; import { ipv8Service } from '@/services/ipv8.service'; +import { isErrorDict } from "@/services/reporting"; +import { Drift } from "@/models/drift.model"; import { average, median } from '@/lib/utils'; import { useInterval } from '@/hooks/useInterval'; +import { useTranslation } from "react-i18next"; -interface Drift { - timestamp: number; - drift: number; -} - export default function Health() { const [drifts, setDrifts] = useState([]); - const ref = useRef(null) + const ref = useRef(null); + const { t } = useTranslation(); useInterval(async () => { let measurements = await ipv8Service.getDrift(); - const now = new Date().getTime() / 1000; - setDrifts(measurements.filter((drift: Drift) => drift.timestamp > now - 11.0)); + if (!(measurements === undefined) && !isErrorDict(measurements)) { + // We ignore errors and correct with the missing information on the next call + const now = new Date().getTime() / 1000; + setDrifts(measurements.filter((drift: Drift) => drift.timestamp > now - 11.0)); - updateCanvas(); + updateCanvas(); + } }, 250, true); useEffect(() => { - (async () => { await ipv8Service.enableDrift(true) })(); + (async () => { + const response = await ipv8Service.enableDrift(true); + if (response === undefined) { + toast.error(`${t("ToastErrorEnableHealth")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorEnableHealth")} ${response.error}`); + } + })(); return () => { - (async () => { await ipv8Service.enableDrift(false) })(); + (async () => { + const response = await ipv8Service.enableDrift(false); + if (response === undefined) { + toast.error(`${t("ToastErrorDisableHealth")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDisableHealth")} ${response.error}`); + } + })(); } }, []); diff --git a/src/tribler/ui/src/pages/Debug/Asyncio/SlowTasks.tsx b/src/tribler/ui/src/pages/Debug/Asyncio/SlowTasks.tsx index def8b57d0b..64276d8acc 100644 --- a/src/tribler/ui/src/pages/Debug/Asyncio/SlowTasks.tsx +++ b/src/tribler/ui/src/pages/Debug/Asyncio/SlowTasks.tsx @@ -1,5 +1,8 @@ import { useEffect, useState } from "react"; +import toast from 'react-hot-toast'; +import { useTranslation } from "react-i18next"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -9,17 +12,36 @@ import { ScrollArea } from "@/components/ui/scroll-area"; export default function SlowTasks() { + const { t } = useTranslation(); const [debug, setDebug] = useState<{ messages?: { message: string }[] }>({}) const [slownessThreshold, setSlownessThreshold] = useState(0.1) useInterval(async () => { - setDebug(await ipv8Service.getAsyncioDebug()); + const response = await ipv8Service.getAsyncioDebug(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setDebug(response); + } }, 5000, true); useEffect(() => { - (async () => { await ipv8Service.setAsyncioDebug(true, slownessThreshold) })(); + (async () => { + const response = await ipv8Service.setAsyncioDebug(true, slownessThreshold); + if (response === undefined) { + toast.error(`${t("ToastErrorEnableAsyncio")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorEnableAsyncio")} ${response.error}`); + } + })(); return () => { - (async () => { await ipv8Service.setAsyncioDebug(false, slownessThreshold) })(); + (async () => { + const response = await ipv8Service.setAsyncioDebug(false, slownessThreshold); + if (response === undefined) { + toast.error(`${t("ToastErrorDisableAsyncio")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDisableAsyncio")} ${response.error}`); + } + })(); } }, []); @@ -40,7 +62,15 @@ export default function SlowTasks() { diff --git a/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx b/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx index b16763e4eb..06aa34d93e 100644 --- a/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx +++ b/src/tribler/ui/src/pages/Debug/Asyncio/Tasks.tsx @@ -2,18 +2,12 @@ import SimpleTable from "@/components/ui/simple-table"; import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; +import { Task } from "@/models/task.model"; import { useInterval } from '@/hooks/useInterval'; import { formatTimeDiff } from "@/lib/utils"; -interface Task { - taskmanager: string; - name: string; - running: boolean; - interval: number; - start_time: number; -} - const taskColumns: ColumnDef[] = [ { accessorKey: "taskmanager", @@ -47,7 +41,11 @@ export default function Tasks() { const [tasks, setTasks] = useState([]) useInterval(async () => { - setTasks((await ipv8Service.getTasks())); + const response = await ipv8Service.getTasks(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setTasks(response); + } }, 5000, true); return ( diff --git a/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx b/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx index 974d5cf2b5..099f828428 100644 --- a/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx +++ b/src/tribler/ui/src/pages/Debug/DHT/Buckets.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Bucket } from "@/models/bucket.model"; import { ColumnDef } from "@tanstack/react-table"; import { formatTimeDiff } from "@/lib/utils"; @@ -32,7 +33,11 @@ export default function Buckets() { const [buckets, setBuckets] = useState([]) useInterval(async () => { - setBuckets((await ipv8Service.getBuckets())); + const response = await ipv8Service.getBuckets(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setBuckets(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/DHT/Statistics.tsx b/src/tribler/ui/src/pages/Debug/DHT/Statistics.tsx index d6ed5c2713..2ca4928472 100644 --- a/src/tribler/ui/src/pages/Debug/DHT/Statistics.tsx +++ b/src/tribler/ui/src/pages/Debug/DHT/Statistics.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { KeyValue } from "@/models/keyvalue.model"; import { ColumnDef } from "@tanstack/react-table"; import { useInterval } from '@/hooks/useInterval'; @@ -25,10 +26,14 @@ export default function Statistics() { useInterval(async () => { let stats: KeyValue[] = []; - for (const [key, value] of Object.entries(await ipv8Service.getDHTStatistics())) { - stats.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value, null, 4) : value }); + const response = await ipv8Service.getDHTStatistics(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + for (const [key, value] of Object.entries(response)) { + stats.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value, null, 4) : value }); + } + setStatistics(stats); } - setStatistics(stats); }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/General/index.tsx b/src/tribler/ui/src/pages/Debug/General/index.tsx index b535d12dbe..277f5329b0 100644 --- a/src/tribler/ui/src/pages/Debug/General/index.tsx +++ b/src/tribler/ui/src/pages/Debug/General/index.tsx @@ -2,6 +2,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { formatBytes } from "@/lib/utils"; import { KeyValue } from "@/models/keyvalue.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import { useInterval } from '@/hooks/useInterval'; @@ -22,14 +23,37 @@ export default function General() { const [stats, setStats] = useState([]) useInterval(async () => { - const ipv8Stats = await triblerService.getIPv8Statistics(); + const newStats = new Array(); + const triblerStats = await triblerService.getTriblerStatistics(); - setStats([ - { key: 'Database size', value: formatBytes(triblerStats.db_size) }, - { key: 'Number of torrents collected', value: triblerStats.num_torrents }, - { key: 'Total IPv8 bytes up', value: formatBytes(ipv8Stats.total_up) }, - { key: 'Total IPv8 bytes down', value: formatBytes(ipv8Stats.total_down) } - ]); + if (triblerStats === undefined || isErrorDict(triblerStats)){ + if (stats) { + newStats.push({ key: 'Database size', value: stats.filter((entry) => entry.key == 'Database size')[0].value }); + newStats.push({ key: 'Number of torrents collected', value: stats.filter((entry) => entry.key == 'Number of torrents collected')[0].value }); + } else { + newStats.push({ key: 'Database size', value: '?' }); + newStats.push({ key: 'Number of torrents collected', value: '?' }); + } + } else { + newStats.push({ key: 'Database size', value: formatBytes(triblerStats.db_size) }); + newStats.push({ key: 'Number of torrents collected', value: "" + triblerStats.num_torrents }); + } + + const ipv8Stats = await triblerService.getIPv8Statistics(); + if (ipv8Stats === undefined || isErrorDict(ipv8Stats)){ + if (stats) { + newStats.push({ key: 'Total IPv8 bytes up', value: stats.filter((entry) => entry.key == 'Total IPv8 bytes up')[0].value }); + newStats.push({ key: 'Total IPv8 bytes down', value: stats.filter((entry) => entry.key == 'Total IPv8 bytes down')[0].value }); + } else { + newStats.push({ key: 'Total IPv8 bytes up', value: '?' }); + newStats.push({ key: 'Total IPv8 bytes down', value: '?' }); + } + } else { + newStats.push({ key: 'Total IPv8 bytes up', value: formatBytes(ipv8Stats.total_up) }); + newStats.push({ key: 'Total IPv8 bytes down', value: formatBytes(ipv8Stats.total_down) }); + } + + setStats(newStats); }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/IPv8/Details.tsx b/src/tribler/ui/src/pages/Debug/IPv8/Details.tsx index 32ae94423f..7c7aa500c8 100644 --- a/src/tribler/ui/src/pages/Debug/IPv8/Details.tsx +++ b/src/tribler/ui/src/pages/Debug/IPv8/Details.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { OverlayMsgStats } from "@/models/overlay.model"; import { ColumnDef } from "@tanstack/react-table"; import { useInterval } from '@/hooks/useInterval'; @@ -50,28 +51,32 @@ export default function Details() { useInterval(async () => { let stats: OverlayMsgStats[] = []; - for (var overlayStats of (await ipv8Service.getOverlayStatistics())) { - for (const [communityName, communityStats] of Object.entries(overlayStats)) { - if (Object.entries(communityStats).length === 0) { break } - stats.push({ - name: communityName, - identifier: -1, - num_up: 0, - num_down: 0, - bytes_up: 0, - bytes_down: 0, - first_measured_up: 0, - first_measured_down: 0, - last_measured_up: 0, - last_measured_down: 0, - }); - for (const [msgName, msgStats] of Object.entries(communityStats)) { - msgStats.name = msgName; - stats.push(msgStats); + const response = await ipv8Service.getOverlayStatistics(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + for (var overlayStats of response) { + for (const [communityName, communityStats] of Object.entries(overlayStats)) { + if (Object.entries(communityStats).length === 0) { break } + stats.push({ + name: communityName, + identifier: -1, + num_up: 0, + num_down: 0, + bytes_up: 0, + bytes_down: 0, + first_measured_up: 0, + first_measured_down: 0, + last_measured_up: 0, + last_measured_down: 0, + }); + for (const [msgName, msgStats] of Object.entries(communityStats)) { + msgStats.name = msgName; + stats.push(msgStats); + } } } + setStatistics(stats); } - setStatistics(stats); }, 5000, true); if (statistics.length === 0) { diff --git a/src/tribler/ui/src/pages/Debug/IPv8/Overlays.tsx b/src/tribler/ui/src/pages/Debug/IPv8/Overlays.tsx index 3d72fc525b..1b85992cec 100644 --- a/src/tribler/ui/src/pages/Debug/IPv8/Overlays.tsx +++ b/src/tribler/ui/src/pages/Debug/IPv8/Overlays.tsx @@ -2,6 +2,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Overlay, Peer } from "@/models/overlay.model"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { useInterval } from '@/hooks/useInterval'; @@ -104,7 +105,11 @@ export default function Overlays() { const [selectedOverlay, setSelectedOverlay] = useState() useInterval(async () => { - setOverlays((await ipv8Service.getOverlays())); + const response = await ipv8Service.getOverlays(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setOverlays(response); + } }, 5000, true); // We're not getting resize event for elements within ResizeablePanel, so we track the ResizablePanel itself. diff --git a/src/tribler/ui/src/pages/Debug/Libtorrent/index.tsx b/src/tribler/ui/src/pages/Debug/Libtorrent/index.tsx index 06b014bc85..4fedf847e7 100644 --- a/src/tribler/ui/src/pages/Debug/Libtorrent/index.tsx +++ b/src/tribler/ui/src/pages/Debug/Libtorrent/index.tsx @@ -3,6 +3,7 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrig import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { KeyValue } from "@/models/keyvalue.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import { useInterval } from '@/hooks/useInterval'; @@ -25,17 +26,25 @@ export default function Libtorrent() { const [session, setSession] = useState([]) useInterval(async () => { - let settings = []; - for (const [key, value] of Object.entries(await triblerService.getLibtorrentSettings(hops))) { - settings.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value) : value }); + const libtorrentSettings = await triblerService.getLibtorrentSettings(hops); + if (!(libtorrentSettings === undefined) && !isErrorDict(libtorrentSettings)) { + // Don't bother the user on error, just try again later. + let settings = []; + for (const [key, value] of Object.entries(libtorrentSettings)) { + settings.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value) : value }); + } + setSettings(settings) } - setSettings(settings) - let session = []; - for (const [key, value] of Object.entries(await triblerService.getLibtorrentSession(hops))) { - session.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value) : value }); + const libtorrentSession = await triblerService.getLibtorrentSession(hops); + if (!(libtorrentSession === undefined) && !isErrorDict(libtorrentSession)) { + // Don't bother the user on error, just try again later. + let session = []; + for (const [key, value] of Object.entries(libtorrentSession)) { + session.push({ key: key, value: (typeof value !== 'string') ? JSON.stringify(value) : value }); + } + setSession(session) } - setSession(session) }, 5000, true); diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx index baf8b6d2b5..0e136f51c4 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Circuits.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Circuit } from "@/models/circuit.model"; import { ColumnDef } from "@tanstack/react-table"; import { formatBytes, formatFlags, formatTimeDiff } from "@/lib/utils"; @@ -61,7 +62,11 @@ export default function Circuits() { const [circuits, setCircuits] = useState([]) useInterval(async () => { - setCircuits((await ipv8Service.getCircuits())); + const response = await ipv8Service.getCircuits(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setCircuits(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx index f610678ab7..e67d0d0f4e 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Exits.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Exit } from "@/models/exit.model"; import { ColumnDef } from "@tanstack/react-table"; import { formatBytes, formatTimeDiff } from "@/lib/utils"; @@ -43,7 +44,11 @@ export default function Exits() { const [exits, setExits] = useState([]) useInterval(async () => { - setExits((await ipv8Service.getExits())); + const response = await ipv8Service.getExits(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setExits(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Peers.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Peers.tsx index 159654501e..643876a916 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Peers.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Peers.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Peer } from "@/models/tunnelpeer.model"; import { ColumnDef } from "@tanstack/react-table"; import { formatFlags } from "@/lib/utils"; @@ -37,7 +38,11 @@ export default function Peers() { const [peers, setPeers] = useState([]) useInterval(async () => { - setPeers((await ipv8Service.getTunnelPeers())); + const response = await await ipv8Service.getTunnelPeers(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setPeers(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx index 2f13305f91..1c23af349e 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Relays.tsx @@ -2,6 +2,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { ColumnDef } from "@tanstack/react-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Relay } from "@/models/relay.model"; import { formatBytes, formatTimeDiff } from "@/lib/utils"; import { useInterval } from '@/hooks/useInterval'; @@ -47,7 +48,11 @@ export default function Relays() { const [relays, setRelays] = useState([]) useInterval(async () => { - setRelays((await ipv8Service.getRelays())); + const response = await ipv8Service.getRelays(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setRelays(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx b/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx index 0da6192866..c276e50928 100644 --- a/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx +++ b/src/tribler/ui/src/pages/Debug/Tunnels/Swarms.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useState } from "react"; import { ipv8Service } from "@/services/ipv8.service"; +import { isErrorDict } from "@/services/reporting"; import { Swarm } from "@/models/swarm.model"; import { ColumnDef } from "@tanstack/react-table"; import { formatBytes, formatTimeDiff } from "@/lib/utils"; @@ -55,7 +56,11 @@ export default function Swarms() { const [swarms, setSwarms] = useState([]) useInterval(async () => { - setSwarms((await ipv8Service.getSwarms())); + const response = await ipv8Service.getSwarms(); + if (!(response === undefined) && !isErrorDict(response)) { + // We ignore errors and correct with the missing information on the next call + setSwarms(response); + } }, 5000, true); return diff --git a/src/tribler/ui/src/pages/Downloads/Actions.tsx b/src/tribler/ui/src/pages/Downloads/Actions.tsx index c2be556233..e9c8c1861b 100644 --- a/src/tribler/ui/src/pages/Downloads/Actions.tsx +++ b/src/tribler/ui/src/pages/Downloads/Actions.tsx @@ -1,6 +1,7 @@ import { Download } from "@/models/download.model"; import { triblerService } from "@/services/tribler.service"; - +import { isErrorDict } from "@/services/reporting"; +import toast from 'react-hot-toast'; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -31,23 +32,51 @@ export default function Actions({ selectedDownloads }: { selectedDownloads: Down const onPlay = () => { selectedDownloads.forEach((download) => { - (async () => { await triblerService.resumeDownload(download.infohash) })(); + (async () => { + const response = await triblerService.resumeDownload(download.infohash); + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadPlay")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadPlay")} ${response.error}`); + } + })(); }); } const onPause = () => { selectedDownloads.forEach((download) => { - (async () => { await triblerService.stopDownload(download.infohash) })(); + (async () => { + const response = await triblerService.stopDownload(download.infohash); + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadStop")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadStop")} ${response.error}`); + } + })(); }); } const onRemove = (removeData: boolean) => { selectedDownloads.forEach((download) => { - (async () => { await triblerService.removeDownload(download.infohash, removeData) })(); + (async () => { + const response = await triblerService.removeDownload(download.infohash, removeData); + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadRemove")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadRemove")} ${response.error}`); + } + })(); }); setRemoveDialogOpen(false); } const onRecheck = () => { selectedDownloads.forEach((download) => { - (async () => { await triblerService.recheckDownload(download.infohash) })(); + (async () => { + const response = await triblerService.recheckDownload(download.infohash); + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadCheck")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadCheck")} ${response.error}`); + } + })(); }); } const onExportTorrent = () => { @@ -64,12 +93,25 @@ export default function Actions({ selectedDownloads }: { selectedDownloads: Down } } const onMoveDownloadConfirmed = () => { - triblerService.moveDownload(selectedDownloads[0].infohash, storageLocation); + triblerService.moveDownload(selectedDownloads[0].infohash, storageLocation).then(async (response) => { + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadMove")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadMove")} ${response.error}`); + } + }); setStorageDialogOpen(false); } const onSetHops = (hops: number) => { selectedDownloads.forEach((download) => { - (async () => { await triblerService.setDownloadHops(download.infohash, hops) })(); + (async () => { + const response = await triblerService.setDownloadHops(download.infohash, hops); + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadSetHops")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadSetHops")} ${response.error}`); + } + })(); }); } diff --git a/src/tribler/ui/src/pages/Downloads/Files.tsx b/src/tribler/ui/src/pages/Downloads/Files.tsx index b041c44bd0..5033f93886 100644 --- a/src/tribler/ui/src/pages/Downloads/Files.tsx +++ b/src/tribler/ui/src/pages/Downloads/Files.tsx @@ -1,11 +1,14 @@ +import toast from 'react-hot-toast'; import { ColumnDef } from "@tanstack/react-table"; import { File } from "@/models/file.model"; import { Download } from "@/models/download.model"; -import { useEffect, useRef, useState } from "react"; +import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef, useState } from "react"; +import { isErrorDict } from "@/services/reporting"; import { triblerService } from "@/services/tribler.service"; import SimpleTable from "@/components/ui/simple-table"; import { Checkbox } from "@/components/ui/checkbox"; import { formatBytes, getRowSelection, translateHeader } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; const fileColumns: ColumnDef[] = [ @@ -54,7 +57,18 @@ const fileColumns: ColumnDef[] = [ }, ] +async function updateFiles(setFiles: Dispatch>, infohash: string, initialized: MutableRefObject) { + const response = await triblerService.getDownloadFiles(infohash); + if (response !== undefined && !isErrorDict(response)) { + setFiles(response); + } else { + // Don't bother the user on error, just try again later. + initialized.current = false; + } +} + export default function Files({ download }: { download: Download }) { + const { t } = useTranslation(); const [files, setFiles] = useState([]); const initialized = useRef(false) @@ -78,7 +92,13 @@ export default function Files({ download }: { download: Download }) { } if (shouldUpdate) - triblerService.setDownloadFiles(download.infohash, selectIndices); + triblerService.setDownloadFiles(download.infohash, selectIndices).then((response) => { + if (response === undefined) { + toast.error(`${t("ToastErrorDownloadSetFiles")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorDownloadSetFiles")} ${response.error}`); + } + }); } useEffect(() => { @@ -87,7 +107,7 @@ export default function Files({ download }: { download: Download }) { return; } initialized.current = true; - (async () => setFiles(await triblerService.getDownloadFiles(download.infohash)))(); + updateFiles(setFiles, download.infohash, initialized); }, []); // We'll wait until the API call returns so the selection gets set by initialRowSelection diff --git a/src/tribler/ui/src/pages/Downloads/index.tsx b/src/tribler/ui/src/pages/Downloads/index.tsx index 87f9f37133..f24f000e83 100644 --- a/src/tribler/ui/src/pages/Downloads/index.tsx +++ b/src/tribler/ui/src/pages/Downloads/index.tsx @@ -5,6 +5,7 @@ import { Download } from "@/models/download.model"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress" import { capitalize, formatBytes, translateHeader } from "@/lib/utils"; +import { isErrorDict } from "@/services/reporting"; import { triblerService } from "@/services/tribler.service"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { ColumnDef } from "@tanstack/react-table" @@ -155,9 +156,14 @@ export default function Downloads({ statusFilter }: { statusFilter: number[] }) let infohash = (selectedDownloads.length === 1) ? selectedDownloads[0].infohash : ''; if (infohashes) infohash = infohashes[0] ?? ""; - setDownloads((await triblerService.getDownloads(infohash, !!infohash, !!infohash)).filter((download) => { - return statusFilter.includes(download.status_code); - })) + + // Don't bother the user on error, just try again later. + const response = await triblerService.getDownloads(infohash, !!infohash, !!infohash); + if (response !== undefined && !isErrorDict(response)) { + setDownloads(response.filter((download: Download) => { + return statusFilter.includes(download.status_code); + })); + } } useEffect(() => { diff --git a/src/tribler/ui/src/pages/Popular/index.tsx b/src/tribler/ui/src/pages/Popular/index.tsx index 350286eccb..216a9c60ac 100644 --- a/src/tribler/ui/src/pages/Popular/index.tsx +++ b/src/tribler/ui/src/pages/Popular/index.tsx @@ -2,6 +2,7 @@ import SimpleTable from "@/components/ui/simple-table"; import SaveAs from "@/dialogs/SaveAs"; import { useCallback, useEffect, useMemo, useState } from "react"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { Torrent } from "@/models/torrent.model"; import { ColumnDef } from "@tanstack/react-table"; import { categoryIcon, filterDuplicates, formatBytes, formatTimeAgo, getMagnetLink, translateHeader } from "@/lib/utils"; @@ -60,10 +61,15 @@ export default function Popular() { const [request, setRequest] = useState(""); useInterval(async () => { - const popular = await triblerService.getPopularTorrents(true); - setTorrents(filterDuplicates(popular, 'infohash')); + const popular = await triblerService.getPopularTorrents(); + if (!(popular === undefined) && !isErrorDict(popular)) { + // Don't bother the user on error, just try again later. + setTorrents(filterDuplicates(popular, 'infohash')); + } const remoteQuery = await triblerService.searchTorrentsRemote('', true); - setRequest(remoteQuery.request_uuid); + if (!(remoteQuery === undefined) && !isErrorDict(remoteQuery)) { + setRequest(remoteQuery.request_uuid); + } }, 5000, true); useEffect(() => { diff --git a/src/tribler/ui/src/pages/Search/index.tsx b/src/tribler/ui/src/pages/Search/index.tsx index 7e70c19f78..381c87ca3a 100644 --- a/src/tribler/ui/src/pages/Search/index.tsx +++ b/src/tribler/ui/src/pages/Search/index.tsx @@ -1,6 +1,7 @@ import SimpleTable from "@/components/ui/simple-table"; import { useCallback, useEffect, useMemo, useState } from "react"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { Torrent } from "@/models/torrent.model"; import { ColumnDef } from "@tanstack/react-table"; import { categoryIcon, filterDuplicates, formatBytes, formatTimeAgo, getMagnetLink } from "@/lib/utils"; @@ -74,9 +75,14 @@ export default function Search() { const searchTorrents = async () => { if (!query) return; const localResults = await triblerService.searchTorrentsLocal(query); - setTorrents(filterDuplicates(localResults, 'infohash')); + if (!(localResults === undefined) && !isErrorDict(localResults)) { + // Don't bother the user on error, just try again later. + setTorrents(filterDuplicates(localResults, 'infohash')); + } const remoteQuery = await triblerService.searchTorrentsRemote(query, false); - setRequest(remoteQuery.request_uuid); + if (!(remoteQuery === undefined) && !isErrorDict(remoteQuery)) { + setRequest(remoteQuery.request_uuid); + } } searchTorrents(); }, [query]); diff --git a/src/tribler/ui/src/pages/Settings/Anonymity.tsx b/src/tribler/ui/src/pages/Settings/Anonymity.tsx index 49869b4920..ef06c1f6eb 100644 --- a/src/tribler/ui/src/pages/Settings/Anonymity.tsx +++ b/src/tribler/ui/src/pages/Settings/Anonymity.tsx @@ -1,7 +1,9 @@ import SaveButton from "./SaveButton"; +import toast from 'react-hot-toast'; import { Slider } from "@/components/ui/slider"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -11,7 +13,16 @@ export default function Anonimity() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -48,8 +59,14 @@ export default function Anonimity() { { - if (settings) - await triblerService.setSettings(settings); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } + } }} />
diff --git a/src/tribler/ui/src/pages/Settings/Bandwidth.tsx b/src/tribler/ui/src/pages/Settings/Bandwidth.tsx index 9cbecfa2fc..c3f3120d0c 100644 --- a/src/tribler/ui/src/pages/Settings/Bandwidth.tsx +++ b/src/tribler/ui/src/pages/Settings/Bandwidth.tsx @@ -2,8 +2,10 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import SaveButton from "./SaveButton"; @@ -12,7 +14,16 @@ export default function Bandwith() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -69,8 +80,14 @@ export default function Bandwith() { { - if (settings) - await triblerService.setSettings(settings); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } + } }} /> diff --git a/src/tribler/ui/src/pages/Settings/Connection.tsx b/src/tribler/ui/src/pages/Settings/Connection.tsx index f6c02a38ba..4bbb82ac5b 100644 --- a/src/tribler/ui/src/pages/Settings/Connection.tsx +++ b/src/tribler/ui/src/pages/Settings/Connection.tsx @@ -4,8 +4,10 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import SaveButton from "./SaveButton"; @@ -14,7 +16,16 @@ export default function Connection() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -179,8 +190,14 @@ export default function Connection() { { - if (settings) - await triblerService.setSettings(settings); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } + } }} /> diff --git a/src/tribler/ui/src/pages/Settings/Debugging.tsx b/src/tribler/ui/src/pages/Settings/Debugging.tsx index 3432a375b2..da11289830 100644 --- a/src/tribler/ui/src/pages/Settings/Debugging.tsx +++ b/src/tribler/ui/src/pages/Settings/Debugging.tsx @@ -1,8 +1,10 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import SaveButton from "./SaveButton"; @@ -11,7 +13,16 @@ export default function Debugging() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -62,9 +73,16 @@ export default function Debugging() { { - if (settings) - await triblerService.setSettings(settings); - window.location.reload(); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } else { + window.location.reload(); + } + } }} /> diff --git a/src/tribler/ui/src/pages/Settings/General.tsx b/src/tribler/ui/src/pages/Settings/General.tsx index 595dfe2dd5..1da08249ad 100644 --- a/src/tribler/ui/src/pages/Settings/General.tsx +++ b/src/tribler/ui/src/pages/Settings/General.tsx @@ -3,8 +3,10 @@ import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import SaveButton from "./SaveButton"; import { Input } from "@/components/ui/input"; @@ -14,7 +16,16 @@ export default function General() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -193,8 +204,14 @@ export default function General() { { - if (settings) - await triblerService.setSettings(settings); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } + } }} /> diff --git a/src/tribler/ui/src/pages/Settings/Seeding.tsx b/src/tribler/ui/src/pages/Settings/Seeding.tsx index 9191915579..0cf89fbf27 100644 --- a/src/tribler/ui/src/pages/Settings/Seeding.tsx +++ b/src/tribler/ui/src/pages/Settings/Seeding.tsx @@ -3,8 +3,10 @@ import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radiogroup"; import { Settings } from "@/models/settings.model"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import toast from 'react-hot-toast'; import SaveButton from "./SaveButton"; @@ -13,7 +15,16 @@ export default function Seeding() { const [settings, setSettings] = useState(); if (!settings) { - (async () => { setSettings(await triblerService.getSettings()) })(); + (async () => { + const response = await triblerService.getSettings(); + if (response === undefined) { + toast.error(`${t("ToastErrorGetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorGetSettings")} ${response.error}`); + } else { + setSettings(response); + } + })(); return null; } @@ -98,8 +109,14 @@ export default function Seeding() { { - if (settings) - await triblerService.setSettings(settings); + if (settings){ + const response = await triblerService.setSettings(settings); + if (response === undefined) { + toast.error(`${t("ToastErrorSetSettings")} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorSetSettings")} ${response.error}`); + } + } }} /> diff --git a/src/tribler/ui/src/pages/Settings/Versions.tsx b/src/tribler/ui/src/pages/Settings/Versions.tsx index 4eec5f8d3b..d929c44681 100644 --- a/src/tribler/ui/src/pages/Settings/Versions.tsx +++ b/src/tribler/ui/src/pages/Settings/Versions.tsx @@ -1,52 +1,107 @@ -import { Suspense, useEffect, useState } from 'react'; +import { Suspense, useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { triblerService } from "@/services/tribler.service"; +import { isErrorDict } from "@/services/reporting"; +import { useInterval } from "@/hooks/useInterval"; import { useTranslation } from "react-i18next"; import { RefreshCw } from 'lucide-react'; export default function Versions() { const { t } = useTranslation(); - const [version, setVersion] = useState(); - const [versions, setVersions] = useState(new Array()); - const [newVersion, setNewVersion] = useState(false); - const [canUpgrade, setCanUpgrade] = useState(false); - const [isUpgrading, setIsUpgrading] = useState(false); + const initStateRef = useRef(0); + const [version, setVersion] = useState(); + const [versions, setVersions] = useState(new Array()); + const [newVersion, setNewVersion] = useState(false); + const [canUpgrade, setCanUpgrade] = useState(false); + const [isUpgrading, setIsUpgrading] = useState(false); const clickedImport = (e: React.MouseEvent, old_version: string) => { - triblerService.performUpgrade(); setIsUpgrading(true); + triblerService.performUpgrade().then((response) => { + if (response === undefined) { + toast.error(`${t("ToastErrorUpgradeFailed")} ${t("ToastErrorGenNetworkErr")}`); + setIsUpgrading(false); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorUpgradeFailed")} ${response.error}`); + setIsUpgrading(false); + } + }); } const clickedRemove = (e: React.MouseEvent, old_version: string) => { - triblerService.removeVersion(old_version); - setVersions(versions.filter((v) => v != old_version)); + triblerService.removeVersion(old_version).then((response) => { + if (response === undefined) { + toast.error(`${t("ToastErrorRemoveVersion", {version: old_version})} ${t("ToastErrorGenNetworkErr")}`); + } else if (isErrorDict(response)){ + toast.error(`${t("ToastErrorRemoveVersion", {version: old_version})} ${response.error}`); + } else { + setVersions(versions.filter((v) => v != old_version)); + } + }); } - const useMountEffect = (fun: React.EffectCallback) => useEffect(fun, []) - useMountEffect(() => { - (async () => { - const version = await triblerService.getVersion(); - setVersion(version); + const initVersionInfo = (async () => { + switch(initStateRef.current){ + case 0: { + const version = await triblerService.getVersion(); + if (!(version === undefined) && !isErrorDict(version)) { + setVersion(version); + initStateRef.current = 1; + } else { + break; // Don't bother the user on error, just initialize later. + } + } + case 1: { + var allVersions = await triblerService.getVersions(); + if (!(allVersions === undefined) && !isErrorDict(allVersions)) { + const current_version = allVersions.current; + const versions = (allVersions.versions).filter((v: string) => v != current_version); + setVersions(versions); + initStateRef.current = 2; + } else { + break; // Don't bother the user on error, just initialize later. + } + } + case 2: { + const canUpgrade = await triblerService.canUpgrade(); + if (!(canUpgrade === undefined) && !isErrorDict(canUpgrade)) { + setCanUpgrade(canUpgrade); + initStateRef.current = 3; + } else { + break; // Don't bother the user on error, just initialize later. + } + } + default: { + break; + } + } + }); - var allVersions = await triblerService.getVersions(); - const versions = (allVersions.versions).filter((v: number) => v != allVersions.current); - setVersions(versions); + useEffect(() => { + initVersionInfo(); + }, []); + useInterval(() => { + if (initStateRef.current < 3) { + initVersionInfo(); + } - const newVersion = await triblerService.getNewVersion(); - setNewVersion(newVersion); + triblerService.isUpgrading().then((isUpgrading) => { + if (isUpgrading !== undefined && !isErrorDict(isUpgrading)) { + // Don't bother the user on error, just try again later. + setIsUpgrading(isUpgrading); + } + }); - const canUpgrade = await triblerService.canUpgrade(); - setCanUpgrade(canUpgrade); - })(); - }); - useEffect(() => { - (async () => { - const isUpgrading = await triblerService.isUpgrading(); - setIsUpgrading(isUpgrading) - })(); - }); + triblerService.getNewVersion().then((newVersion) => { + if (newVersion !== undefined && !isErrorDict(newVersion)) { + // Don't bother the user on error, just try again later. + setNewVersion(newVersion); + } + }); + }, 5000); return (
@@ -71,7 +126,7 @@ export default function Versions() { { - versions.reduce((r, e) => r.push(e, e, e, e) && r, []).map(function(old_version: string, i: number){ + versions.reduce((r: string[], e: string) => {r.push(e, e, e, e); return r;}, new Array()).map(function(old_version: string, i: number){ switch (i % 4){ case 0: { return () diff --git a/src/tribler/ui/src/services/ipv8.service.ts b/src/tribler/ui/src/services/ipv8.service.ts index 2b405a1c31..10d08f8e50 100644 --- a/src/tribler/ui/src/services/ipv8.service.ts +++ b/src/tribler/ui/src/services/ipv8.service.ts @@ -1,7 +1,14 @@ +import { Bucket, DHTStats, Values } from "@/models/bucket.model"; import { Circuit } from "@/models/circuit.model"; -import { OverlayStats } from "@/models/overlay.model"; -import axios, { AxiosInstance } from "axios"; -import { handleHTTPError } from "./reporting"; +import { Drift } from "@/models/drift.model"; +import { Exit } from "@/models/exit.model"; +import { Overlay, OverlayStats } from "@/models/overlay.model"; +import { Relay } from "@/models/relay.model"; +import { Swarm } from "@/models/swarm.model"; +import { Task } from "@/models/task.model"; +import { Peer } from "@/models/tunnelpeer.model"; +import axios, { AxiosError, AxiosInstance } from "axios"; +import { ErrorDict, formatAxiosError, handleHTTPError } from "./reporting"; export class IPv8Service { @@ -16,64 +23,124 @@ export class IPv8Service { } - async enableDrift(enable: boolean): Promise { - return (await this.http.put('/asyncio/drift', { enable })).data.success; + async enableDrift(enable: boolean): Promise { + try { + return (await this.http.put('/asyncio/drift', { enable })).data.success; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getDrift() { - return (await this.http.get('/asyncio/drift')).data.measurements; + async getDrift(): Promise { + try { + return (await this.http.get('/asyncio/drift')).data.measurements; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getTasks() { - return (await this.http.get('/asyncio/tasks')).data.tasks; + async getTasks(): Promise { + try { + return (await this.http.get('/asyncio/tasks')).data.tasks; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async setAsyncioDebug(enable: boolean, slownessThreshold: number): Promise { - return (await this.http.put('/asyncio/debug', {enable: enable, slow_callback_duration: slownessThreshold})).data.success; + async setAsyncioDebug(enable: boolean, slownessThreshold: number): Promise { + try { + return (await this.http.put('/asyncio/debug', {enable: enable, slow_callback_duration: slownessThreshold})).data.success; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getAsyncioDebug(): Promise { - return (await this.http.get('/asyncio/debug')).data; + async getAsyncioDebug(): Promise { + try { + return (await this.http.get('/asyncio/debug')).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getOverlays() { - return (await this.http.get('/overlays')).data.overlays; + async getOverlays(): Promise { + try { + return (await this.http.get('/overlays')).data.overlays; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getOverlayStatistics(): Promise { - return (await this.http.get('/overlays/statistics')).data.statistics; + async getOverlayStatistics(): Promise { + try { + return (await this.http.get('/overlays/statistics')).data.statistics; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getTunnelPeers() { - return (await this.http.get('/tunnel/peers')).data.peers; + async getTunnelPeers(): Promise { + try { + return (await this.http.get('/tunnel/peers')).data.peers; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getCircuits(): Promise { - return (await this.http.get('/tunnel/circuits')).data.circuits; + async getCircuits(): Promise { + try { + return (await this.http.get('/tunnel/circuits')).data.circuits; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getRelays() { - return (await this.http.get('/tunnel/relays')).data.relays; + async getRelays(): Promise { + try { + return (await this.http.get('/tunnel/relays')).data.relays; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getExits() { - return (await this.http.get('/tunnel/exits')).data.exits; + async getExits(): Promise { + try { + return (await this.http.get('/tunnel/exits')).data.exits; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getSwarms() { - return (await this.http.get('/tunnel/swarms')).data.swarms; + async getSwarms(): Promise { + try { + return (await this.http.get('/tunnel/swarms')).data.swarms; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getDHTStatistics() { - return (await this.http.get('/dht/statistics')).data.statistics; - } + async getDHTStatistics(): Promise { + try { + return (await this.http.get('/dht/statistics')).data.statistics; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } + } - async getBuckets() { - return (await this.http.get('/dht/buckets')).data.buckets; - } + async getBuckets(): Promise { + try { + return (await this.http.get('/dht/buckets')).data.buckets; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } + } - async lookupDHTValue(hash: string) { - return this.http.get(`/dht/values/${hash}`); + async lookupDHTValue(hash: string): Promise { + try { + return this.http.get(`/dht/values/${hash}`); + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } } diff --git a/src/tribler/ui/src/services/reporting.ts b/src/tribler/ui/src/services/reporting.ts index cdb673fbca..675e7446cf 100644 --- a/src/tribler/ui/src/services/reporting.ts +++ b/src/tribler/ui/src/services/reporting.ts @@ -1,9 +1,15 @@ import axios, { AxiosError } from "axios"; +export interface ErrorDict { [error: string]: string; }; + +export function isErrorDict(object: any): object is ErrorDict { + return (typeof object === 'object') && ('error' in object); +} + export function handleHTTPError(error: Error | AxiosError) { const error_popup_text = document.querySelector("#error_popup_text"); if (!error_popup_text){ - return Promise.reject(error); + return; } if (axios.isAxiosError(error) && error.response?.data?.error?.message){ error_popup_text.textContent = error.response.data.error.message.replace(/(?:\n)/g, '\r\n'); @@ -18,5 +24,23 @@ export function handleHTTPError(error: Error | AxiosError) { // Unhide if we were hidden error_popup.classList.toggle("hidden"); } - return Promise.reject(error); +} + +export function formatAxiosError(error: Error | AxiosError): ErrorDict | undefined { + if (axios.isAxiosError(error)) { + if (!error.response) { + // This is a network error, see https://github.com/axios/axios?tab=readme-ov-file#error-types + // We don't need to do anything, but the GUI should not crash on this + return undefined; + } + var handled = error.response.data?.error?.handled + if (error.response.data.error.handled == false) { + // This is an error that conforms to the internal unhandled error format: ask the user what to do + handleHTTPError(error); + } + // This is some (probably expected) REST API error + return error.response.data; + } + // No idea what this is: make it someone else's problem + throw error; } diff --git a/src/tribler/ui/src/services/tribler.service.ts b/src/tribler/ui/src/services/tribler.service.ts index de593d8316..60d3fefffe 100644 --- a/src/tribler/ui/src/services/tribler.service.ts +++ b/src/tribler/ui/src/services/tribler.service.ts @@ -5,7 +5,7 @@ import { Path } from "@/models/path.model"; import { GuiSettings, Settings } from "@/models/settings.model"; import { Torrent } from "@/models/torrent.model"; import axios, { AxiosError, AxiosInstance } from "axios"; -import { handleHTTPError } from "./reporting"; +import { ErrorDict, formatAxiosError, handleHTTPError } from "./reporting"; const OnError = (event: MessageEvent) => { @@ -48,186 +48,311 @@ export class TriblerService { // Downloads - async getDownloads(infohash: string = '', getPeers: boolean = false, getPieces: boolean = false): Promise { - return (await this.http.get(`/downloads?infohash=${infohash}&get_peers=${+getPeers}&get_pieces=${+getPieces}`)).data.downloads; + async getDownloads(infohash: string = '', getPeers: boolean = false, getPieces: boolean = false): Promise { + try { + return (await this.http.get(`/downloads?infohash=${infohash}&get_peers=${+getPeers}&get_pieces=${+getPieces}`)).data.downloads; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getDownloadFiles(infohash: string): Promise { - return (await this.http.get(`/downloads/${infohash}/files`)).data.files; + async getDownloadFiles(infohash: string): Promise { + try { + return (await this.http.get(`/downloads/${infohash}/files`)).data.files; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async startDownload(uri: string, params: DownloadConfig = {}): Promise { - return (await this.http.put('/downloads', { ...params, uri: uri })).data.started; + async startDownload(uri: string, params: DownloadConfig = {}): Promise { + try { + return (await this.http.put('/downloads', { ...params, uri: uri })).data.started; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async startDownloadFromFile(torrent: File, params: DownloadConfig = {}): Promise { - return (await this.http.put('/downloads', torrent, { - params: params, - headers: { - 'Content-Type': 'applications/x-bittorrent' - } - })).data.started; + async startDownloadFromFile(torrent: File, params: DownloadConfig = {}): Promise { + try { + return (await this.http.put('/downloads', torrent, { + params: params, + headers: { + 'Content-Type': 'applications/x-bittorrent' + } + })).data.started; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async stopDownload(infohash: string): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { state: 'stop' }); - return response.data.modified; + async stopDownload(infohash: string): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { state: 'stop' })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async resumeDownload(infohash: string): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { state: 'resume' }); - return response.data.modified; + async resumeDownload(infohash: string): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { state: 'resume' })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async recheckDownload(infohash: string): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { state: 'recheck' }); - return response.data.modified; + async recheckDownload(infohash: string): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { state: 'recheck' })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async moveDownload(infohash: string, dest_dir: string): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { state: 'move_storage', dest_dir: dest_dir }); - return response.data.modified; + async moveDownload(infohash: string, dest_dir: string): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { state: 'move_storage', dest_dir: dest_dir })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async setDownloadHops(infohash: string, anon_hops: number): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { anon_hops: anon_hops }); - return response.data.modified; + async setDownloadHops(infohash: string, anon_hops: number): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { anon_hops: anon_hops })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async setDownloadFiles(infohash: string, selected_files: number[]): Promise { - const response = await this.http.patch(`/downloads/${infohash}`, { selected_files: selected_files }); - return response.data.modified; + async setDownloadFiles(infohash: string, selected_files: number[]): Promise { + try { + return (await this.http.patch(`/downloads/${infohash}`, { selected_files: selected_files })).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async removeDownload(infohash: string, removeData: boolean): Promise { - const response = await this.http.delete(`/downloads/${infohash}`, { data: { remove_data: (removeData) ? 1 : 0 } }); - return await response.data.removed; + async removeDownload(infohash: string, removeData: boolean): Promise { + try { + return (await this.http.delete(`/downloads/${infohash}`, { data: { remove_data: (removeData) ? 1 : 0 } })).data.removed; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } // Statistics - async getIPv8Statistics() { - return (await this.http.get('/statistics/ipv8')).data.ipv8_statistics; + async getIPv8Statistics(): Promise { + try { + return (await this.http.get('/statistics/ipv8')).data.ipv8_statistics; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getTriblerStatistics() { - return (await this.http.get('/statistics/tribler')).data.tribler_statistics; + async getTriblerStatistics(): Promise { + try { + return (await this.http.get('/statistics/tribler')).data.tribler_statistics; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } // Torrents / search - async getMetainfo(uri: string) { + async getMetainfo(uri: string): Promise { try { return (await this.http.get(`/torrentinfo?uri=${uri}`)).data; - } - catch (error) { - if (axios.isAxiosError(error)) { - return error.response?.data; - } + } catch (error) { + return formatAxiosError(error as Error | AxiosError); } } - async getMetainfoFromFile(torrent: File) { - return (await this.http.put('/torrentinfo', torrent, { - headers: { - 'Content-Type': 'applications/x-bittorrent' - } - })).data; + async getMetainfoFromFile(torrent: File): Promise { + try { + return (await this.http.put('/torrentinfo', torrent, { + headers: { + 'Content-Type': 'applications/x-bittorrent' + } + })).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getPopularTorrents(hide_xxx: boolean): Promise { - return (await this.http.get(`/metadata/torrents/popular?metadata_type=300&metadata_type=220&include_total=1&first=1&last=50&hide_xxx=${+hide_xxx}`)).data.results; + async getPopularTorrents(): Promise { + try { + return (await this.http.get(`/metadata/torrents/popular?metadata_type=300&metadata_type=220&include_total=1&first=1&last=50`)).data.results; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getTorrentHealth(infohash: string): Promise<{ infohash: string, num_seeders: number, num_leechers: number, last_tracker_check: number }> { - return (await this.http.get(`/metadata/torrents/${infohash}/health`)).data; + async getTorrentHealth(infohash: string): Promise { + try { + return (await this.http.get(`/metadata/torrents/${infohash}/health`)).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getCompletions(txt_filter: string): Promise { - return (await this.http.get(`/metadata/search/completions?q=${txt_filter}`)).data.completions; + async getCompletions(txt_filter: string): Promise { + try { + return (await this.http.get(`/metadata/search/completions?q=${txt_filter}`)).data.completions; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async searchTorrentsLocal(txt_filter: string): Promise { - return (await this.http.get(`/metadata/search/local?first=1&last=200&metadata_type=300&exclude_deleted=1&fts_text=${txt_filter}`)).data.results; + async searchTorrentsLocal(txt_filter: string): Promise { + try { + return (await this.http.get(`/metadata/search/local?first=1&last=200&metadata_type=300&exclude_deleted=1&fts_text=${txt_filter}`)).data.results; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async searchTorrentsRemote(txt_filter: string, popular: boolean): Promise<{ request_uuid: string, peers: string[] }> { - return (await this.http.put(`/search/remote?fts_text=${txt_filter}&popular=${+popular}&metadata_type=300&exclude_deleted=1`)).data; + async searchTorrentsRemote(txt_filter: string, popular: boolean): Promise { + try { + return (await this.http.put(`/search/remote?fts_text=${txt_filter}&popular=${+popular}&metadata_type=300&exclude_deleted=1`)).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } // Settings - async getSettings(): Promise { - const settings = (await this.http.get('/settings')).data.settings; - this.guiSettings = {...settings?.ui, ...this.guiSettings}; - return settings + async getSettings(): Promise { + try { + const settings = (await this.http.get('/settings')).data.settings; + this.guiSettings = {...settings?.ui, ...this.guiSettings}; + return settings + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async setSettings(settings: Partial): Promise { - this.guiSettings = {...settings?.ui, ...this.guiSettings}; - return (await this.http.post('/settings', settings)).data.modified; + async setSettings(settings: Partial): Promise { + try { + this.guiSettings = {...settings?.ui, ...this.guiSettings}; + return (await this.http.post('/settings', settings)).data.modified; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getLibtorrentSession(hops: number) { - return (await this.http.get(`/libtorrent/session?hop=${hops}`)).data.session; + async getLibtorrentSession(hops: number): Promise { + try { + return (await this.http.get(`/libtorrent/session?hop=${hops}`)).data.session; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getLibtorrentSettings(hops: number) { - return (await this.http.get(`/libtorrent/settings?hop=${hops}`)).data.settings; + async getLibtorrentSettings(hops: number): Promise { + try { + return (await this.http.get(`/libtorrent/settings?hop=${hops}`)).data.settings; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } // Versions - async getVersion() { - return (await this.http.get(`/versioning/versions/current`)).data.version; + async getVersion(): Promise { + try { + return (await this.http.get(`/versioning/versions/current`)).data.version; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getNewVersion() { - const version_info_json = (await this.http.get(`/versioning/versions/check`)).data; - return (version_info_json.has_version ? version_info_json.new_version : false); + async getNewVersion(): Promise { + try { + const version_info_json = (await this.http.get(`/versioning/versions/check`)).data; + return (version_info_json.has_version ? version_info_json.new_version : false); + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async getVersions() { - return (await this.http.get(`/versioning/versions`)).data; + async getVersions(): Promise { + try { + return (await this.http.get(`/versioning/versions`)).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async canUpgrade() { - return (await this.http.get(`/versioning/upgrade/available`)).data.can_upgrade; + async canUpgrade(): Promise { + try { + return (await this.http.get(`/versioning/upgrade/available`)).data.can_upgrade; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async isUpgrading() { - return (await this.http.get(`/versioning/upgrade/working`)).data.running; + async isUpgrading(): Promise { + try { + return (await this.http.get(`/versioning/upgrade/working`)).data.running; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async performUpgrade() { - return (await this.http.post(`/versioning/upgrade`)) + async performUpgrade(): Promise { + try { + return (await this.http.post(`/versioning/upgrade`)).data.success; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async removeVersion(version_str: string) { - return (await this.http.delete(`/versioning/versions/${version_str}`)) + async removeVersion(version_str: string): Promise { + try { + return (await this.http.delete(`/versioning/versions/${version_str}`)).data.success; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } // Misc - async browseFiles(path: string, showFiles: boolean): Promise<{ current: string, paths: Path[] }> { - return (await this.http.get(`/files/browse?path=${path}&files=${+showFiles}`)).data; + async browseFiles(path: string, showFiles: boolean): Promise { + try { + return (await this.http.get(`/files/browse?path=${path}&files=${+showFiles}`)).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async listFiles(path: string, recursively: boolean): Promise<{ paths: Path[] }> { - return (await this.http.get(`/files/list?path=${path}&recursively=${+recursively}`)).data; + async listFiles(path: string, recursively: boolean): Promise { + try { + return (await this.http.get(`/files/list?path=${path}&recursively=${+recursively}`)).data; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async createTorrent(name: string, description: string, files: string[], exportDir: string, download: boolean) { - return (await this.http.post(`/createtorrent?download=${+download}`, { - name: name, - description: description, - files: files, - export_dir: exportDir - })).data.torrent; + async createTorrent(name: string, description: string, files: string[], exportDir: string, download: boolean): Promise { + try { + return (await this.http.post(`/createtorrent?download=${+download}`, { + name: name, + description: description, + files: files, + export_dir: exportDir + })).data.torrent; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } - async shutdown(): Promise { - return (await this.http.put(`/shutdown`)).data.shutdown; - + async shutdown(): Promise { + try { + return (await this.http.put(`/shutdown`)).data.shutdown; + } catch (error) { + return formatAxiosError(error as Error | AxiosError); + } } }