mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat(releases): incognito mode (#1282)
* feat(web): incognito mode * removed unused variable * move RandomLinuxIsos into utils/index
This commit is contained in:
parent
3580472cbd
commit
da365da17c
3 changed files with 231 additions and 134 deletions
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
useTable,
|
useTable,
|
||||||
|
@ -16,7 +16,9 @@ import {
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { EmptyListState } from "@components/emptystates";
|
import { EmptyListState } from "@components/emptystates";
|
||||||
import * as Icons from "@components/Icons";
|
import * as Icons from "@components/Icons";
|
||||||
|
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||||
import * as DataTable from "@components/data-table";
|
import * as DataTable from "@components/data-table";
|
||||||
|
import { RandomLinuxIsos } from "@utils/index";
|
||||||
|
|
||||||
// This is a custom filter UI for selecting
|
// This is a custom filter UI for selecting
|
||||||
// a unique option from a list
|
// a unique option from a list
|
||||||
|
@ -201,14 +203,46 @@ export const ActivityTable = () => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const [modifiedData, setModifiedData] = useState<Release[]>([]);
|
||||||
|
const [showLinuxIsos, setShowLinuxIsos] = useState(false);
|
||||||
|
|
||||||
|
const toggleReleaseNames = () => {
|
||||||
|
setShowLinuxIsos(!showLinuxIsos);
|
||||||
|
if (!showLinuxIsos && data && data.data) {
|
||||||
|
const randomNames = RandomLinuxIsos(data.data.length);
|
||||||
|
const newData: Release[] = data.data.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
torrent_name: `${randomNames[index]}.iso`,
|
||||||
|
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
||||||
|
}));
|
||||||
|
setModifiedData(newData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = showLinuxIsos ? modifiedData : (data?.data ?? []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col mt-12">
|
<div className="flex flex-col mt-12 relative">
|
||||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
<h3 className="text-2xl font-medium leading-6 text-black dark:text-white">
|
||||||
Recent activity
|
Recent activity
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Table columns={columns} data={data?.data ?? []} />
|
<Table columns={columns} data={displayData} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleReleaseNames}
|
||||||
|
className="p-2 absolute -bottom-8 right-0 bg-gray-750 text-white rounded-full opacity-10 hover:opacity-100 transition-opacity duration-300"
|
||||||
|
aria-label="Toggle view"
|
||||||
|
title="Go incognito"
|
||||||
|
>
|
||||||
|
{showLinuxIsos ? (
|
||||||
|
<EyeIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<EyeSlashIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
|
import { CellProps, Column, useFilters, usePagination, useSortBy, useTable } from "react-table";
|
||||||
import {
|
import {
|
||||||
|
@ -12,7 +12,9 @@ import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon
|
ChevronRightIcon
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
|
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/solid";
|
||||||
|
|
||||||
|
import { RandomLinuxIsos } from "@utils/index";
|
||||||
import { APIClient } from "@api/APIClient";
|
import { APIClient } from "@api/APIClient";
|
||||||
import { EmptyListState } from "@components/emptystates";
|
import { EmptyListState } from "@components/emptystates";
|
||||||
|
|
||||||
|
@ -148,7 +150,25 @@ export const ReleaseTable = () => {
|
||||||
staleTime: 5000
|
staleTime: 5000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the state and functions returned from useTable to build your UI
|
const [modifiedData, setModifiedData] = useState<Release[]>([]);
|
||||||
|
const [showLinuxIsos, setShowLinuxIsos] = useState(false);
|
||||||
|
|
||||||
|
const toggleReleaseNames = () => {
|
||||||
|
setShowLinuxIsos(!showLinuxIsos);
|
||||||
|
if (!showLinuxIsos && data && data.data) {
|
||||||
|
const randomNames = RandomLinuxIsos(data.data.length);
|
||||||
|
const newData: Release[] = data.data.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
torrent_name: `${randomNames[index]}.iso`,
|
||||||
|
indexer: index % 2 === 0 ? "distrowatch" : "linuxtracker"
|
||||||
|
}));
|
||||||
|
setModifiedData(newData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayData = showLinuxIsos ? modifiedData : (data?.data ?? []);
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
|
@ -170,7 +190,7 @@ export const ReleaseTable = () => {
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
data: data && isSuccess ? data.data : [],
|
data: displayData, // Use displayData here
|
||||||
initialState: {
|
initialState: {
|
||||||
pageIndex: queryPageIndex,
|
pageIndex: queryPageIndex,
|
||||||
pageSize: queryPageSize,
|
pageSize: queryPageSize,
|
||||||
|
@ -392,138 +412,154 @@ export const ReleaseTable = () => {
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
|
<div className="relative">
|
||||||
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
<div className="bg-white dark:bg-gray-800 border border-gray-250 dark:border-gray-775 shadow-table rounded-md overflow-auto">
|
||||||
<thead className="bg-gray-100 dark:bg-gray-850">
|
<table {...getTableProps()} className="min-w-full rounded-md divide-y divide-gray-200 dark:divide-gray-750">
|
||||||
{headerGroups.map((headerGroup) => {
|
<thead className="bg-gray-100 dark:bg-gray-850">
|
||||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
{headerGroups.map((headerGroup) => {
|
||||||
return (
|
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||||
<tr key={rowKey} {...rowRest}>
|
return (
|
||||||
{headerGroup.headers.map((column) => {
|
<tr key={rowKey} {...rowRest}>
|
||||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
{headerGroup.headers.map((column) => {
|
||||||
return (
|
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||||
|
return (
|
||||||
// Add the sorting props to control sorting. For this example
|
// Add the sorting props to control sorting. For this example
|
||||||
// we can add them into the header props
|
// we can add them into the header props
|
||||||
<th
|
<th
|
||||||
key={`${rowKey}-${columnKey}`}
|
key={`${rowKey}-${columnKey}`}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775"
|
className="first:pl-5 first:rounded-tl-md last:rounded-tr-md pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left uppercase group text-gray-600 dark:text-gray-400 transition hover:bg-gray-200 dark:hover:bg-gray-775"
|
||||||
{...columnRest}
|
{...columnRest}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<>{column.render("Header")}</>
|
<>{column.render("Header")}</>
|
||||||
{/* Add a sort direction indicator */}
|
{/* Add a sort direction indicator */}
|
||||||
<span>
|
<span>
|
||||||
{column.isSorted ? (
|
{column.isSorted ? (
|
||||||
column.isSortedDesc ? (
|
column.isSortedDesc ? (
|
||||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
||||||
)
|
)}
|
||||||
) : (
|
</span>
|
||||||
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
</div>
|
||||||
)}
|
</th>
|
||||||
</span>
|
);
|
||||||
</div>
|
})}
|
||||||
</th>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</thead>
|
||||||
);
|
<tbody
|
||||||
})}
|
{...getTableBodyProps()}
|
||||||
</thead>
|
className="divide-y divide-gray-150 dark:divide-gray-750"
|
||||||
<tbody
|
>
|
||||||
{...getTableBodyProps()}
|
{page.map((row) => {
|
||||||
className="divide-y divide-gray-150 dark:divide-gray-750"
|
prepareRow(row);
|
||||||
>
|
|
||||||
{page.map((row) => {
|
|
||||||
prepareRow(row);
|
|
||||||
|
|
||||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||||
return (
|
return (
|
||||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||||
{row.cells.map((cell) => {
|
{row.cells.map((cell) => {
|
||||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={cellRowKey}
|
key={cellRowKey}
|
||||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||||
role="cell"
|
role="cell"
|
||||||
{...cellRowRest}
|
{...cellRowRest}
|
||||||
>
|
>
|
||||||
<>{cell.render("Cell")}</>
|
<>{cell.render("Cell")}</>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between flex-1 sm:hidden">
|
<div className="flex justify-between flex-1 sm:hidden">
|
||||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="flex items-baseline gap-x-2">
|
<div className="flex items-baseline gap-x-2">
|
||||||
<span className="text-sm text-gray-700 dark:text-gray-500">
|
<span className="text-sm text-gray-700 dark:text-gray-500">
|
||||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||||
</span>
|
</span>
|
||||||
<label>
|
<label>
|
||||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||||
<select
|
<select
|
||||||
className="py-1 pl-2 pr-8 text-sm block w-full border-gray-300 rounded-md shadow-sm cursor-pointer transition-colors dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-200 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
className="py-1 pl-2 pr-8 text-sm block w-full border-gray-300 rounded-md shadow-sm cursor-pointer transition-colors dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-200 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||||
value={pageSize}
|
value={pageSize}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setPageSize(Number(e.target.value));
|
setPageSize(Number(e.target.value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[5, 10, 20, 50].map(pageSize => (
|
{[5, 10, 20, 50].map(pageSize => (
|
||||||
<option key={pageSize} value={pageSize}>
|
<option key={pageSize} value={pageSize}>
|
||||||
{pageSize} entries
|
{pageSize} entries
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
<DataTable.PageButton
|
<DataTable.PageButton
|
||||||
className="rounded-l-md"
|
className="rounded-l-md"
|
||||||
onClick={() => gotoPage(0)}
|
onClick={() => gotoPage(0)}
|
||||||
disabled={!canPreviousPage}
|
disabled={!canPreviousPage}
|
||||||
>
|
>
|
||||||
<span className="sr-only">First</span>
|
<span className="sr-only">First</span>
|
||||||
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true" />
|
<ChevronDoubleLeftIcon className="w-4 h-4" aria-hidden="true" />
|
||||||
</DataTable.PageButton>
|
</DataTable.PageButton>
|
||||||
<DataTable.PageButton
|
<DataTable.PageButton
|
||||||
className="pl-1 pr-2"
|
className="pl-1 pr-2"
|
||||||
onClick={() => previousPage()}
|
onClick={() => previousPage()}
|
||||||
disabled={!canPreviousPage}
|
disabled={!canPreviousPage}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true" />
|
<ChevronLeftIcon className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||||
<span>Prev</span>
|
<span>Prev</span>
|
||||||
</DataTable.PageButton>
|
</DataTable.PageButton>
|
||||||
<DataTable.PageButton
|
<DataTable.PageButton
|
||||||
className="pl-2 pr-1"
|
className="pl-2 pr-1"
|
||||||
onClick={() => nextPage()}
|
onClick={() => nextPage()}
|
||||||
disabled={!canNextPage}>
|
disabled={!canNextPage}>
|
||||||
<span>Next</span>
|
<span>Next</span>
|
||||||
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true" />
|
<ChevronRightIcon className="w-4 h-4 ml-1" aria-hidden="true" />
|
||||||
</DataTable.PageButton>
|
</DataTable.PageButton>
|
||||||
<DataTable.PageButton
|
<DataTable.PageButton
|
||||||
className="rounded-r-md"
|
className="rounded-r-md"
|
||||||
onClick={() => gotoPage(pageCount - 1)}
|
onClick={() => gotoPage(pageCount - 1)}
|
||||||
disabled={!canNextPage}
|
disabled={!canNextPage}
|
||||||
>
|
>
|
||||||
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true" />
|
<ChevronDoubleRightIcon className="w-4 h-4" aria-hidden="true" />
|
||||||
<span className="sr-only">Last</span>
|
<span className="sr-only">Last</span>
|
||||||
</DataTable.PageButton>
|
</DataTable.PageButton>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute -bottom-11 right-0 p-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleReleaseNames}
|
||||||
|
className="p-2 absolute bottom-0 right-0 bg-gray-750 text-white rounded-full opacity-10 hover:opacity-100 transition-opacity duration-300"
|
||||||
|
aria-label="Toggle view"
|
||||||
|
title="Go incognito"
|
||||||
|
>
|
||||||
|
{showLinuxIsos ? (
|
||||||
|
<EyeIcon className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<EyeSlashIcon className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,7 +70,7 @@ export function slugify(str: string) {
|
||||||
// it might not work for some edge cases. Test your code!
|
// it might not work for some edge cases. Test your code!
|
||||||
export const get = <T> (obj: T, path: string|Array<any>, defValue?: string) => {
|
export const get = <T> (obj: T, path: string|Array<any>, defValue?: string) => {
|
||||||
// If path is not defined or it has false value
|
// If path is not defined or it has false value
|
||||||
if (!path)
|
if (!path)
|
||||||
return undefined;
|
return undefined;
|
||||||
// Check if path is string or array. Regex : ensure that we do not have '.' and brackets.
|
// Check if path is string or array. Regex : ensure that we do not have '.' and brackets.
|
||||||
// Regex explained: https://regexr.com/58j0k
|
// Regex explained: https://regexr.com/58j0k
|
||||||
|
@ -83,3 +83,30 @@ export const get = <T> (obj: T, path: string|Array<any>, defValue?: string) => {
|
||||||
// If found value is undefined return default value; otherwise return the value
|
// If found value is undefined return default value; otherwise return the value
|
||||||
return result === undefined ? defValue : result;
|
return result === undefined ? defValue : result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RandomLinuxIsos = (count: number) => {
|
||||||
|
const linuxIsos = [
|
||||||
|
"ubuntu-20.04.4-lts-focal-fossa-desktop-amd64-secure-boot",
|
||||||
|
"debian-11.3.0-bullseye-amd64-DVD-1-with-nonfree-firmware-netinst",
|
||||||
|
"fedora-36-workstation-x86_64-live-iso-with-rpmfusion-free-and-nonfree",
|
||||||
|
"archlinux-2023.04.01-x86_64-advanced-installation-environment",
|
||||||
|
"linuxmint-20.3-uma-cinnamon-64bit-full-multimedia-support-edition",
|
||||||
|
"centos-stream-9-x86_64-dvd1-full-install-iso-with-extended-repositories",
|
||||||
|
"opensuse-tumbleweed-20230415-DVD-x86_64-full-packaged-desktop-environments",
|
||||||
|
"manjaro-kde-21.1.6-210917-linux514-full-hardware-support-edition",
|
||||||
|
"elementaryos-6.1-odin-amd64-20230104-iso-with-pantheon-desktop-environment",
|
||||||
|
"pop_os-21.10-amd64-nvidia-proprietary-drivers-included-live",
|
||||||
|
"kali-linux-2023.2-live-amd64-iso-with-persistent-storage-and-custom-tools",
|
||||||
|
"zorin-os-16-pro-ultimate-edition-64-bit-r1-iso-with-windows-app-support",
|
||||||
|
"endeavouros-2023.04.15-x86_64-iso-with-offline-installer-and-xfce4",
|
||||||
|
"mx-linux-21.2-aarch64-xfce-iso-with-ahs-enabled-kernel-and-snapshot-feature",
|
||||||
|
"solus-4.3-budgie-desktop-environment-full-iso-with-software-center",
|
||||||
|
"slackware-15.0-install-dvd-iso-with-extended-documentation-and-extras",
|
||||||
|
"alpine-standard-3.15.0-x86_64-iso-for-container-and-server-use",
|
||||||
|
"gentoo-livecd-amd64-minimal-20230407-stage3-tarball-included",
|
||||||
|
"peppermint-11-20210903-amd64-iso-with-hybrid-lxde-xfce-desktop",
|
||||||
|
"deepin-20.3-amd64-iso-with-deepin-desktop-environment-and-app-store"
|
||||||
|
];
|
||||||
|
|
||||||
|
return Array.from({ length: count }, () => linuxIsos[Math.floor(Math.random() * linuxIsos.length)]);
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue