Package manager UI-related tweaks (#4382)

* Add Plugins Path setting
* Fix/improve cache invalidation
* Hide load error when collapsing package source
* Package manager style tweaks
* Show error if installed packages query failed
* Prevent "No packages found" flicker
* Show <unknown> if empty version
* Always show latest version, highlight if new version available
* Fix issues with non-unique cross-source package ids
* Don't wrap id, version and date
* Decrease collapse button padding
* Display description for scraper packages
* Fix default packages population
* Change default package path to community
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
DingDongSoLong4 2023-12-22 05:05:53 +02:00 committed by GitHub
parent 23b4d4f1e0
commit a1bd7cf817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 611 additions and 500 deletions

View file

@ -9,6 +9,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
generatedPath
metadataPath
scrapersPath
pluginsPath
cachePath
blobsPath
blobsStorage

View file

@ -55,7 +55,7 @@ query InstalledPluginPackages {
query InstalledPluginPackagesStatus {
installedPackages(type: Plugin) {
...PackageData
upgrade {
source_package {
...PackageData
}
}

View file

@ -129,7 +129,7 @@ query InstalledScraperPackages {
query InstalledScraperPackagesStatus {
installedPackages(type: Scraper) {
...PackageData
upgrade {
source_package {
...PackageData
}
}

View file

@ -73,6 +73,8 @@ input ConfigGeneralInput {
metadataPath: String
"Path to scrapers"
scrapersPath: String
"Path to plugins"
pluginsPath: String
"Path to cache"
cachePath: String
"Path to blobs - required for filesystem blob storage"
@ -189,6 +191,8 @@ type ConfigGeneralResult {
configFilePath: String!
"Path to scrapers"
scrapersPath: String!
"Path to plugins"
pluginsPath: String!
"Path to cache"
cachePath: String!
"Path to blobs - required for filesystem blob storage"

View file

@ -12,8 +12,8 @@ type Package {
sourceURL: String!
"The available upgraded version of this package"
upgrade: Package
"The version of this package currently available from the remote source"
source_package: Package
metadata: Map!
}

View file

@ -104,6 +104,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
refreshScraperCache := false
refreshScraperSource := false
existingScrapersPath := c.GetScrapersPath()
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
@ -111,9 +112,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
}
refreshScraperCache = true
refreshScraperSource = true
c.Set(config.ScrapersPath, input.ScrapersPath)
}
refreshPluginCache := false
refreshPluginSource := false
existingPluginsPath := c.GetPluginsPath()
if input.PluginsPath != nil && existingPluginsPath != *input.PluginsPath {
if err := validateDir(config.PluginsPath, *input.PluginsPath, false); err != nil {
return makeConfigGeneralResult(), err
}
refreshPluginCache = true
refreshPluginSource = true
c.Set(config.PluginsPath, input.PluginsPath)
}
existingMetadataPath := c.GetMetadataPath()
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
if err := validateDir(config.Metadata, *input.MetadataPath, true); err != nil {
@ -347,13 +362,11 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
}
refreshScraperSource := false
if input.ScraperPackageSources != nil {
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
refreshScraperSource = true
}
refreshPluginSource := false
if input.PluginPackageSources != nil {
c.Set(config.PluginPackageSources, input.PluginPackageSources)
refreshPluginSource = true
@ -367,6 +380,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
if refreshScraperCache {
manager.GetInstance().RefreshScraperCache()
}
if refreshPluginCache {
manager.GetInstance().RefreshPluginCache()
}
if refreshStreamManager {
manager.GetInstance().RefreshStreamManager()
}

View file

@ -87,6 +87,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
MetadataPath: config.GetMetadataPath(),
ConfigFilePath: config.GetConfigFile(),
ScrapersPath: config.GetScrapersPath(),
PluginsPath: config.GetPluginsPath(),
CachePath: config.GetCachePath(),
BlobsPath: config.GetBlobsPath(),
BlobsStorage: config.GetBlobsStorage(),

View file

@ -98,11 +98,24 @@ func sortedPackageSpecKeys[V any](m map[models.PackageSpecInput]V) []models.Pack
}
sort.Slice(keys, func(i, j int) bool {
if strings.EqualFold(keys[i].ID, keys[j].ID) {
return keys[i].ID < keys[j].ID
a := keys[i]
b := keys[j]
aID := a.ID
bID := b.ID
if aID == bID {
return a.SourceURL < b.SourceURL
}
return strings.ToLower(keys[i].ID) < strings.ToLower(keys[j].ID)
aIDL := strings.ToLower(aID)
bIDL := strings.ToLower(bID)
if aIDL == bIDL {
return aID < bID
}
return aIDL < bIDL
})
return keys
@ -129,9 +142,9 @@ func (r *queryResolver) getInstalledPackagesWithUpgrades(ctx context.Context, pm
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
v := packageStatusIndex[k]
p := manifestToPackage(*v.Local)
if v.Upgradable() {
if v.Remote != nil {
pp := remotePackageToPackage(*v.Remote, allRemoteList)
p.Upgrade = pp
p.SourcePackage = pp
}
ret[i] = p
i++
@ -146,19 +159,19 @@ func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageTy
return nil, err
}
installed, err := pm.ListInstalled(ctx)
if err != nil {
return nil, err
}
var ret []*Package
if sliceutil.Contains(graphql.CollectAllFields(ctx), "upgrade") {
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
if err != nil {
return nil, err
}
} else {
installed, err := pm.ListInstalled(ctx)
if err != nil {
return nil, err
}
ret = make([]*Package, len(installed))
i := 0
for _, k := range sortedPackageSpecKeys(installed) {

View file

@ -138,7 +138,7 @@ const (
PluginsSettingPrefix = PluginsSetting + "."
DisabledPlugins = "plugins.disabled"
sourceDefaultPath = "stable"
sourceDefaultPath = "community"
sourceDefaultName = "Community (stable)"
PluginPackageSources = "plugins.package_sources"
@ -1666,16 +1666,16 @@ func (i *Config) setDefaultValues() {
i.main.SetDefault(NoProxy, noProxyDefault)
// set default package sources
i.main.SetDefault(PluginPackageSources, map[string]string{
"name": sourceDefaultName,
"url": pluginPackageSourcesDefault,
"local_path": sourceDefaultPath,
})
i.main.SetDefault(ScraperPackageSources, map[string]string{
"name": sourceDefaultName,
"url": scraperPackageSourcesDefault,
"local_path": sourceDefaultPath,
})
i.main.SetDefault(PluginPackageSources, []map[string]string{{
"name": sourceDefaultName,
"url": pluginPackageSourcesDefault,
"localpath": sourceDefaultPath,
}})
i.main.SetDefault(ScraperPackageSources, []map[string]string{{
"name": sourceDefaultName,
"url": scraperPackageSourcesDefault,
"localpath": sourceDefaultPath,
}})
}
// setExistingSystemDefaults sets config options that are new and unset in an existing install,

View file

@ -1,14 +1,14 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql";
import {
evictQueries,
getClient,
queryAvailablePluginPackages,
useInstallPluginPackages,
useInstalledPluginPackages,
useInstalledPluginPackagesStatus,
useUninstallPluginPackages,
useUpdatePluginPackages,
mutateInstallPluginPackages,
mutateUninstallPluginPackages,
mutateUpdatePluginPackages,
pluginMutationImpactedQueries,
} from "src/core/StashService";
import { useMonitorJob } from "src/utils/job";
import {
@ -20,95 +20,59 @@ import { useSettings } from "./context";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { SettingSection } from "./SettingSection";
const impactedPackageChangeQueries = [
GQL.PluginsDocument,
GQL.PluginTasksDocument,
GQL.InstalledPluginPackagesDocument,
GQL.InstalledPluginPackagesStatusDocument,
];
export const InstalledPluginPackages: React.FC = () => {
const [loadUpgrades, setLoadUpgrades] = useState(false);
const [jobID, setJobID] = useState<string>();
const { job } = useMonitorJob(jobID, () => onPackageChanges());
const { data: installedPlugins, refetch: refetchPackages1 } =
useInstalledPluginPackages({
skip: loadUpgrades,
});
const {
data: withStatus,
refetch: refetchPackages2,
loading: statusLoading,
} = useInstalledPluginPackagesStatus({
skip: !loadUpgrades,
});
const [updatePackages] = useUpdatePluginPackages();
const [uninstallPackages] = useUninstallPluginPackages();
const { data, previousData, refetch, loading, error } =
useInstalledPluginPackages(loadUpgrades);
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
const r = await updatePackages({
variables: {
packages,
},
});
const r = await mutateUpdatePluginPackages(packages);
setJobID(r.data?.updatePackages);
}
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
const r = await uninstallPackages({
variables: {
packages,
},
});
const r = await mutateUninstallPluginPackages(packages);
setJobID(r.data?.uninstallPackages);
}
function refetchPackages() {
refetchPackages1();
refetchPackages2();
}
function onPackageChanges() {
// job is complete, refresh all local data
const ac = getClient();
evictQueries(ac.cache, impactedPackageChangeQueries);
evictQueries(ac.cache, pluginMutationImpactedQueries);
}
function onCheckForUpdates() {
if (!loadUpgrades) {
setLoadUpgrades(true);
} else {
refetchPackages();
refetch();
}
}
const installedPackages = useMemo(() => {
if (withStatus?.installedPackages) {
return withStatus.installedPackages;
}
return installedPlugins?.installedPackages ?? [];
}, [installedPlugins, withStatus]);
const loading = !!job || statusLoading;
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
// to prevent displaying a "No packages found" message
const installedPackages =
data?.installedPackages ?? previousData?.installedPackages ?? [];
return (
<SettingSection headingID="config.plugins.installed_plugins">
<div className="package-manager">
<InstalledPackages
loading={loading}
loading={!!job || loading}
error={error?.message}
packages={installedPackages}
onCheckForUpdates={onCheckForUpdates}
onUpdatePackages={(packages) =>
onUpdatePackages(
packages.map((p) => ({
id: p.package_id,
sourceURL: p.upgrade!.sourceURL,
sourceURL: p.sourceURL,
}))
)
}
@ -120,7 +84,7 @@ export const InstalledPluginPackages: React.FC = () => {
}))
)
}
updatesLoaded={loadUpgrades}
updatesLoaded={loadUpgrades && !loading}
/>
</div>
</SettingSection>
@ -130,18 +94,11 @@ export const InstalledPluginPackages: React.FC = () => {
export const AvailablePluginPackages: React.FC = () => {
const { general, loading: configLoading, error, saveGeneral } = useSettings();
const [sources, setSources] = useState<GQL.PackageSource[]>();
const [jobID, setJobID] = useState<string>();
const { job } = useMonitorJob(jobID, () => onPackageChanges());
const [installPackages] = useInstallPluginPackages();
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
const r = await installPackages({
variables: {
packages,
},
});
const r = await mutateInstallPluginPackages(packages);
setJobID(r.data?.installPackages);
}
@ -149,15 +106,9 @@ export const AvailablePluginPackages: React.FC = () => {
function onPackageChanges() {
// job is complete, refresh all local data
const ac = getClient();
evictQueries(ac.cache, impactedPackageChangeQueries);
evictQueries(ac.cache, pluginMutationImpactedQueries);
}
useEffect(() => {
if (!sources && !configLoading && general.pluginPackageSources) {
setSources(general.pluginPackageSources);
}
}, [sources, configLoading, general.pluginPackageSources]);
async function loadSource(source: string): Promise<RemotePackage[]> {
const { data } = await queryAvailablePluginPackages(source);
return data.availablePackages;
@ -167,10 +118,6 @@ export const AvailablePluginPackages: React.FC = () => {
saveGeneral({
pluginPackageSources: [...(general.pluginPackageSources ?? []), source],
});
setSources((prev) => {
return [...(prev ?? []), source];
});
}
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
@ -179,10 +126,6 @@ export const AvailablePluginPackages: React.FC = () => {
s.url === existing.url ? changed : s
),
});
setSources((prev) => {
return prev?.map((s) => (s.url === existing.url ? changed : s));
});
}
function deleteSource(source: GQL.PackageSource) {
@ -191,10 +134,6 @@ export const AvailablePluginPackages: React.FC = () => {
(s) => s.url !== source.url
),
});
setSources((prev) => {
return prev?.filter((s) => s.url !== source.url);
});
}
function renderDescription(pkg: RemotePackage) {
@ -208,6 +147,8 @@ export const AvailablePluginPackages: React.FC = () => {
const loading = !!job;
const sources = general?.pluginPackageSources ?? [];
return (
<SettingSection headingID="config.plugins.available_plugins">
<div className="package-manager">
@ -216,7 +157,7 @@ export const AvailablePluginPackages: React.FC = () => {
onInstallPackages={onInstallPackages}
renderDescription={renderDescription}
loadSource={(source) => loadSource(source)}
sources={sources ?? []}
sources={sources}
addSource={addSource}
editSource={editSource}
deleteSource={deleteSource}

View file

@ -1,14 +1,14 @@
import React, { useEffect, useState, useMemo } from "react";
import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql";
import {
evictQueries,
getClient,
queryAvailableScraperPackages,
useInstallScraperPackages,
useInstalledScraperPackages,
useInstalledScraperPackagesStatus,
useUninstallScraperPackages,
useUpdateScraperPackages,
mutateUpdateScraperPackages,
mutateUninstallScraperPackages,
mutateInstallScraperPackages,
scraperMutationImpactedQueries,
} from "src/core/StashService";
import { useMonitorJob } from "src/utils/job";
import {
@ -20,96 +20,59 @@ import { useSettings } from "./context";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { SettingSection } from "./SettingSection";
const impactedPackageChangeQueries = [
GQL.ListPerformerScrapersDocument,
GQL.ListSceneScrapersDocument,
GQL.ListMovieScrapersDocument,
GQL.InstalledScraperPackagesDocument,
GQL.InstalledScraperPackagesStatusDocument,
];
export const InstalledScraperPackages: React.FC = () => {
const [loadUpgrades, setLoadUpgrades] = useState(false);
const [jobID, setJobID] = useState<string>();
const { job } = useMonitorJob(jobID, () => onPackageChanges());
const { data: installedScrapers, refetch: refetchPackages1 } =
useInstalledScraperPackages({
skip: loadUpgrades,
});
const {
data: withStatus,
refetch: refetchPackages2,
loading: statusLoading,
} = useInstalledScraperPackagesStatus({
skip: !loadUpgrades,
});
const [updatePackages] = useUpdateScraperPackages();
const [uninstallPackages] = useUninstallScraperPackages();
const { data, previousData, refetch, loading, error } =
useInstalledScraperPackages(loadUpgrades);
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
const r = await updatePackages({
variables: {
packages,
},
});
const r = await mutateUpdateScraperPackages(packages);
setJobID(r.data?.updatePackages);
}
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
const r = await uninstallPackages({
variables: {
packages,
},
});
const r = await mutateUninstallScraperPackages(packages);
setJobID(r.data?.uninstallPackages);
}
function refetchPackages() {
refetchPackages1();
refetchPackages2();
}
function onPackageChanges() {
// job is complete, refresh all local data
const ac = getClient();
evictQueries(ac.cache, impactedPackageChangeQueries);
evictQueries(ac.cache, scraperMutationImpactedQueries);
}
function onCheckForUpdates() {
if (!loadUpgrades) {
setLoadUpgrades(true);
} else {
refetchPackages();
refetch();
}
}
const installedPackages = useMemo(() => {
if (withStatus?.installedPackages) {
return withStatus.installedPackages;
}
return installedScrapers?.installedPackages ?? [];
}, [installedScrapers, withStatus]);
const loading = !!job || statusLoading;
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
// to prevent displaying a "No packages found" message
const installedPackages =
data?.installedPackages ?? previousData?.installedPackages ?? [];
return (
<SettingSection headingID="config.scraping.installed_scrapers">
<div className="package-manager">
<InstalledPackages
loading={loading}
loading={!!job || loading}
error={error?.message}
packages={installedPackages}
onCheckForUpdates={onCheckForUpdates}
onUpdatePackages={(packages) =>
onUpdatePackages(
packages.map((p) => ({
id: p.package_id,
sourceURL: p.upgrade!.sourceURL,
sourceURL: p.sourceURL,
}))
)
}
@ -121,7 +84,7 @@ export const InstalledScraperPackages: React.FC = () => {
}))
)
}
updatesLoaded={loadUpgrades}
updatesLoaded={loadUpgrades && !loading}
/>
</div>
</SettingSection>
@ -131,18 +94,11 @@ export const InstalledScraperPackages: React.FC = () => {
export const AvailableScraperPackages: React.FC = () => {
const { general, loading: configLoading, error, saveGeneral } = useSettings();
const [sources, setSources] = useState<GQL.PackageSource[]>();
const [jobID, setJobID] = useState<string>();
const { job } = useMonitorJob(jobID, () => onPackageChanges());
const [installPackages] = useInstallScraperPackages();
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
const r = await installPackages({
variables: {
packages,
},
});
const r = await mutateInstallScraperPackages(packages);
setJobID(r.data?.installPackages);
}
@ -150,15 +106,9 @@ export const AvailableScraperPackages: React.FC = () => {
function onPackageChanges() {
// job is complete, refresh all local data
const ac = getClient();
evictQueries(ac.cache, impactedPackageChangeQueries);
evictQueries(ac.cache, scraperMutationImpactedQueries);
}
useEffect(() => {
if (!sources && !configLoading && general.scraperPackageSources) {
setSources(general.scraperPackageSources);
}
}, [sources, configLoading, general.scraperPackageSources]);
async function loadSource(source: string): Promise<RemotePackage[]> {
const { data } = await queryAvailableScraperPackages(source);
return data.availablePackages;
@ -168,10 +118,6 @@ export const AvailableScraperPackages: React.FC = () => {
saveGeneral({
scraperPackageSources: [...(general.scraperPackageSources ?? []), source],
});
setSources((prev) => {
return [...(prev ?? []), source];
});
}
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
@ -180,10 +126,6 @@ export const AvailableScraperPackages: React.FC = () => {
s.url === existing.url ? changed : s
),
});
setSources((prev) => {
return prev?.map((s) => (s.url === existing.url ? changed : s));
});
}
function deleteSource(source: GQL.PackageSource) {
@ -192,10 +134,12 @@ export const AvailableScraperPackages: React.FC = () => {
(s) => s.url !== source.url
),
});
}
setSources((prev) => {
return prev?.filter((s) => s.url !== source.url);
});
function renderDescription(pkg: RemotePackage) {
if (pkg.metadata.description) {
return pkg.metadata.description;
}
}
if (error) return <h1>{error.message}</h1>;
@ -203,14 +147,17 @@ export const AvailableScraperPackages: React.FC = () => {
const loading = !!job;
const sources = general?.scraperPackageSources ?? [];
return (
<SettingSection headingID="config.scraping.available_scrapers">
<div className="package-manager">
<AvailablePackages
loading={loading}
onInstallPackages={onInstallPackages}
renderDescription={renderDescription}
loadSource={(source) => loadSource(source)}
sources={sources ?? []}
sources={sources}
addSource={addSource}
editSource={editSource}
deleteSource={deleteSource}

View file

@ -137,6 +137,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
onChange={(v) => saveGeneral({ scrapersPath: v })}
/>
<StringSetting
id="plugins-path"
headingID="config.general.plugins_path.heading"
subHeadingID="config.general.plugins_path.description"
value={general.pluginsPath ?? undefined}
onChange={(v) => saveGeneral({ pluginsPath: v })}
/>
<StringSetting
id="metadata-path"
headingID="config.general.metadata_path.heading"

View file

@ -3,7 +3,9 @@ import React, { useState, useMemo, useEffect } from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { Icon } from "../Icon";
import cx from "classnames";
import {
faAnglesUp,
faChevronDown,
faChevronRight,
faRotate,
@ -17,7 +19,19 @@ import { LoadingIndicator } from "../LoadingIndicator";
import { ApolloError } from "@apollo/client";
import { ClearableInput } from "../ClearableInput";
function formatDate(intl: IntlShape, date: string | undefined | null) {
function packageKey(
pkg: Pick<GQL.Package, "package_id" | "sourceURL">
): string {
return `${pkg.sourceURL}-${pkg.package_id}`;
}
function displayVersion(intl: IntlShape, version: string | undefined | null) {
if (!version) return intl.formatMessage({ id: "package_manager.unknown" });
return version;
}
function displayDate(intl: IntlShape, date: string | undefined | null) {
if (!date) return;
const d = new Date(date);
@ -59,14 +73,17 @@ const InstalledPackageRow: React.FC<{
}> = ({ loading, pkg, selected, togglePackage, updatesLoaded }) => {
const intl = useIntl();
function rowClassname() {
if (pkg.upgrade?.version) {
return "package-update-available";
}
}
const updateAvailable = useMemo(() => {
if (!updatesLoaded) return false;
if (!pkg.date || !pkg.source_package?.date) return false;
const pkgDate = new Date(pkg.date);
const upgradeDate = new Date(pkg.source_package.date);
return upgradeDate > pkgDate;
}, [updatesLoaded, pkg]);
return (
<tr className={rowClassname()}>
<tr className={cx({ "package-update-available": updateAvailable })}>
<td>
<Form.Check
checked={selected}
@ -79,17 +96,22 @@ const InstalledPackageRow: React.FC<{
<span className="package-id">{pkg.package_id}</span>
</td>
<td>
<span className="package-version">{pkg.version}</span>
<span className="package-date">{formatDate(intl, pkg.date)}</span>
<span className="package-version">
{displayVersion(intl, pkg.version)}
</span>
<span className="package-date">{displayDate(intl, pkg.date)}</span>
</td>
{updatesLoaded ? (
{updatesLoaded && pkg.source_package && (
<td>
<span className="package-version">{pkg.upgrade?.version}</span>
<span className="package-date">
{formatDate(intl, pkg.upgrade?.date)}
<span className="package-latest-version">
{displayVersion(intl, pkg.source_package.version)}
{updateAvailable && <Icon icon={faAnglesUp} />}
</span>
<span className="package-latest-date">
{displayDate(intl, pkg.source_package.date)}
</span>
</td>
) : undefined}
)}
</tr>
);
};
@ -97,6 +119,7 @@ const InstalledPackageRow: React.FC<{
const InstalledPackagesList: React.FC<{
filter: string;
loading?: boolean;
error?: string;
updatesLoaded: boolean;
packages: InstalledPackage[];
checkedPackages: InstalledPackage[];
@ -108,12 +131,13 @@ const InstalledPackagesList: React.FC<{
setCheckedPackages,
updatesLoaded,
loading,
error,
}) => {
const checkedMap = useMemo(() => {
const map: Record<string, boolean> = {};
checkedPackages.forEach((pkg) => {
map[`${pkg.sourceURL}-${pkg.package_id}`] = true;
});
for (const pkg of checkedPackages) {
map[packageKey(pkg)] = true;
}
return map;
}, [checkedPackages]);
@ -134,19 +158,54 @@ const InstalledPackagesList: React.FC<{
setCheckedPackages((prev) => {
if (prev.includes(pkg)) {
return prev.filter((n) => n.package_id !== pkg.package_id);
return prev.filter((n) => packageKey(n) !== packageKey(pkg));
} else {
return prev.concat(pkg);
return [...prev, pkg];
}
});
}
function renderBody() {
if (error) {
return (
<tr>
<td />
<td colSpan={1000} className="source-error">
<Icon icon={faWarning} />
<span>{error}</span>
</td>
</tr>
);
}
if (filteredPackages.length === 0) {
return (
<tr className="package-manager-no-results">
<td colSpan={1000}>
<FormattedMessage id="package_manager.no_packages" />
</td>
</tr>
);
}
return filteredPackages.map((pkg) => (
<InstalledPackageRow
key={packageKey(pkg)}
loading={loading}
pkg={pkg}
selected={checkedMap[packageKey(pkg)] ?? false}
togglePackage={() => togglePackage(pkg)}
updatesLoaded={updatesLoaded}
/>
));
}
return (
<div className="package-manager-table-container">
<Table>
<thead>
<tr>
<th className="button-cell">
<th className="check-cell">
<Form.Check
checked={allChecked ?? false}
onChange={toggleAllChecked}
@ -166,28 +225,7 @@ const InstalledPackagesList: React.FC<{
) : undefined}
</tr>
</thead>
<tbody>
{filteredPackages.length === 0 ? (
<tr className="package-manager-no-results">
<td colSpan={updatesLoaded ? 4 : 3}>
<FormattedMessage id="package_manager.no_packages" />
</td>
</tr>
) : (
filteredPackages.map((pkg) => (
<InstalledPackageRow
key={`${pkg.sourceURL}-${pkg.package_id}`}
loading={loading}
pkg={pkg}
selected={
checkedMap[`${pkg.sourceURL}-${pkg.package_id}`] ?? false
}
togglePackage={() => togglePackage(pkg)}
updatesLoaded={updatesLoaded}
/>
))
)}
</tbody>
<tbody>{renderBody()}</tbody>
</Table>
</div>
);
@ -213,42 +251,40 @@ const InstalledPackagesToolbar: React.FC<{
const intl = useIntl();
return (
<div className="package-manager-toolbar">
<div>
<ClearableInput
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
value={filter}
setValue={(v) => setFilter(v)}
/>
</div>
<div>
<Button
variant="primary"
onClick={() => onCheckForUpdates()}
disabled={loading}
>
<FormattedMessage id="package_manager.check_for_updates" />
</Button>
<Button
variant="primary"
disabled={!checkedPackages.length || loading}
onClick={() => onUpdatePackages()}
>
<FormattedMessage id="package_manager.update" />
</Button>
<Button
variant="danger"
disabled={!checkedPackages.length || loading}
onClick={() => onUninstallPackages()}
>
<FormattedMessage id="package_manager.uninstall" />
</Button>
</div>
<ClearableInput
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
value={filter}
setValue={(v) => setFilter(v)}
/>
<div className="flex-grow-1" />
<Button
variant="primary"
onClick={() => onCheckForUpdates()}
disabled={loading}
>
<FormattedMessage id="package_manager.check_for_updates" />
</Button>
<Button
variant="primary"
disabled={!checkedPackages.length || loading}
onClick={() => onUpdatePackages()}
>
<FormattedMessage id="package_manager.update" />
</Button>
<Button
variant="danger"
disabled={!checkedPackages.length || loading}
onClick={() => onUninstallPackages()}
>
<FormattedMessage id="package_manager.uninstall" />
</Button>
</div>
);
};
export const InstalledPackages: React.FC<{
loading?: boolean;
error?: string;
packages: InstalledPackage[];
updatesLoaded: boolean;
onCheckForUpdates: () => void;
@ -261,6 +297,7 @@ export const InstalledPackages: React.FC<{
onUpdatePackages,
onUninstallPackages,
loading,
error,
}) => {
const [checkedPackages, setCheckedPackages] = useState<InstalledPackage[]>(
[]
@ -275,7 +312,7 @@ export const InstalledPackages: React.FC<{
useEffect(() => {
setCheckedPackages((prev) => {
const newVal = prev.filter((pkg) =>
packages.find((p) => p.package_id === pkg.package_id)
packages.find((p) => packageKey(p) === packageKey(pkg))
);
if (newVal.length !== prev.length) {
return newVal;
@ -316,6 +353,7 @@ export const InstalledPackages: React.FC<{
<InstalledPackagesList
filter={filter}
loading={loading}
error={error}
packages={packages}
// use original checked packages so that check boxes are not affected by filter
checkedPackages={checkedPackages}
@ -353,31 +391,28 @@ const AvailablePackagesToolbar: React.FC<{
return (
<div className="package-manager-toolbar">
<div>
<ClearableInput
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
value={filter}
setValue={(v) => setFilter(v)}
/>
{hasSelectedPackages && (
<Button
size="sm"
variant="primary"
onClick={() => setSelectedOnly(!selectedOnly)}
>
<FormattedMessage id={selectedOnlyId} />
</Button>
)}
</div>
<div>
<ClearableInput
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
value={filter}
setValue={(v) => setFilter(v)}
/>
{hasSelectedPackages && (
<Button
size="sm"
variant="primary"
disabled={!hasSelectedPackages || loading}
onClick={() => onInstallPackages()}
onClick={() => setSelectedOnly(!selectedOnly)}
>
<FormattedMessage id="package_manager.install" />
<FormattedMessage id={selectedOnlyId} />
</Button>
</div>
)}
<div className="flex-grow-1" />
<Button
variant="primary"
disabled={!hasSelectedPackages || loading}
onClick={() => onInstallPackages()}
>
<FormattedMessage id="package_manager.install" />
</Button>
</div>
);
};
@ -552,7 +587,7 @@ const AvailablePackageRow: React.FC<{
}
return (
<tr key={pkg.package_id}>
<tr>
<td colSpan={2}>
<Form.Check
checked={selected ?? false}
@ -565,8 +600,10 @@ const AvailablePackageRow: React.FC<{
<span className="package-id">{pkg.package_id}</span>
</td>
<td>
<span className="package-version">{pkg.version}</span>
<span className="package-date">{formatDate(intl, pkg.date)}</span>
<span className="package-version">
{displayVersion(intl, pkg.version)}
</span>
<span className="package-date">{displayDate(intl, pkg.date)}</span>
</td>
<td>
{renderRequiredBy()}
@ -655,36 +692,62 @@ const SourcePackagesList: React.FC<{
}
function toggleSourceOpen() {
if (packages === undefined) {
loadPackages();
if (sourceOpen) {
setLoadError(undefined);
setSourceOpen(false);
} else {
if (packages === undefined) {
loadPackages();
}
setSourceOpen(true);
}
}
function renderContents() {
if (loading) {
return (
<tr>
<td colSpan={2}></td>
<td colSpan={3}>
<LoadingIndicator inline small />
</td>
</tr>
);
}
setSourceOpen((prev) => !prev);
}
if (loadError) {
return (
<tr>
<td colSpan={2}></td>
<td colSpan={3} className="source-error">
<Icon icon={faWarning} />
<span>{loadError}</span>
<Button
size="sm"
variant="secondary"
onClick={() => loadPackages()}
title={intl.formatMessage({ id: "actions.reload" })}
>
<Icon icon={faRotate} />
</Button>
</td>
</tr>
);
}
function renderCollapseButton() {
return (
<Button
variant="minimal"
size="sm"
className="package-collapse-button"
onClick={() => toggleSourceOpen()}
>
<Icon icon={sourceOpen ? faChevronDown : faChevronRight} />
</Button>
);
}
if (!sourceOpen) {
return null;
}
const children = useMemo(() => {
function getRequiredPackages(pkg: RemotePackage) {
const ret: RemotePackage[] = [];
pkg.requires.forEach((r) => {
for (const r of pkg.requires) {
const found = packages?.find((p) => p.package_id === r.package_id);
if (found && !ret.includes(found)) {
ret.push(found);
ret.push(...getRequiredPackages(found));
}
});
}
return ret;
}
@ -698,10 +761,7 @@ const SourcePackagesList: React.FC<{
return prev.filter((n) => n.package_id !== pkg.package_id);
} else {
// also include required packages
const toAdd = [pkg];
toAdd.push(...getRequiredPackages(pkg));
return prev.concat(...toAdd);
return [...prev, pkg, ...getRequiredPackages(pkg)];
}
});
}
@ -711,27 +771,19 @@ const SourcePackagesList: React.FC<{
key={pkg.package_id}
disabled={disabled}
pkg={pkg}
requiredBy={selectedPackages.filter((p) => {
return p.requires.find((r) => r.package_id === pkg.package_id);
})}
requiredBy={selectedPackages.filter((p) =>
p.requires.some((r) => r.package_id === pkg.package_id)
)}
selected={checkedMap[pkg.package_id] ?? false}
togglePackage={() => togglePackage(pkg)}
renderDescription={renderDescription}
/>
));
}, [
filteredPackages,
disabled,
checkedMap,
selectedPackages,
setSelectedPackages,
packages,
renderDescription,
]);
}
return (
<>
<tr key={source.url} className="package-source">
<tr className="package-source">
<td>
{packages !== undefined ? (
<Form.Check
@ -741,8 +793,20 @@ const SourcePackagesList: React.FC<{
/>
) : undefined}
</td>
<td>{renderCollapseButton()}</td>
<td colSpan={2} onClick={() => toggleSourceOpen()}>
<td className="source-collapse">
<Button
variant="minimal"
size="sm"
onClick={() => toggleSourceOpen()}
>
<Icon icon={sourceOpen ? faChevronDown : faChevronRight} />
</Button>
</td>
<td
className="source-name"
colSpan={2}
onClick={() => toggleSourceOpen()}
>
<span>{source.name ?? source.url}</span>
</td>
<td className="source-controls">
@ -764,32 +828,7 @@ const SourcePackagesList: React.FC<{
</Button>
</td>
</tr>
{loading ? (
<tr>
<td colSpan={2}></td>
<td colSpan={3}>
<LoadingIndicator inline small />
</td>
</tr>
) : undefined}
{loadError ? (
<tr>
<td colSpan={2}></td>
<td colSpan={3} className="source-error">
<Icon icon={faWarning} />
<span>{loadError}</span>
<Button
size="sm"
variant="secondary"
onClick={() => loadPackages()}
title={intl.formatMessage({ id: "actions.reload" })}
>
<Icon icon={faRotate} />
</Button>
</td>
</tr>
) : undefined}
{sourceOpen && !loading && children}
{renderContents()}
</>
);
};
@ -847,6 +886,58 @@ const AvailablePackagesList: React.FC<{
});
}
function renderBody() {
if (sources.length === 0) {
return (
<tr className="package-manager-no-results">
<td colSpan={5}>
<FormattedMessage id="package_manager.no_sources" />
<br />
<Button
size="sm"
variant="success"
onClick={() => setAddingSource(true)}
>
<FormattedMessage id="package_manager.add_source" />
</Button>
</td>
</tr>
);
}
return (
<>
{sources.map((src) => (
<SourcePackagesList
key={src.url}
filter={filter}
disabled={loading}
source={src}
renderDescription={renderDescription}
loadSource={() => loadSource(src.url)}
selectedOnly={selectedOnly}
selectedPackages={selectedPackages[src.url] ?? []}
setSelectedPackages={(v) => setSelectedSourcePackages(src, v)}
editSource={() => setEditingSource(src)}
deleteSource={() => setDeletingSource(src)}
/>
))}
<tr className="add-package-source">
<td colSpan={2}></td>
<td colSpan={3}>
<Button
size="sm"
variant="success"
onClick={() => setAddingSource(true)}
>
<FormattedMessage id="package_manager.add_source" />
</Button>
</td>
</tr>
</>
);
}
return (
<>
<AlertModal
@ -880,8 +971,8 @@ const AvailablePackagesList: React.FC<{
<Table>
<thead>
<tr>
<th className="button-cell"></th>
<th className="button-cell"></th>
<th className="check-cell"></th>
<th className="collapse-cell"></th>
<th>
<FormattedMessage id="package_manager.package" />
</th>
@ -893,49 +984,7 @@ const AvailablePackagesList: React.FC<{
</th>
</tr>
</thead>
<tbody>
{sources.length === 0 ? (
<tr className="package-manager-no-results">
<td colSpan={5}>
<FormattedMessage id="package_manager.no_sources" />
<br />
<Button
size="sm"
variant="success"
onClick={() => setAddingSource(true)}
>
<FormattedMessage id="package_manager.add_source" />
</Button>
</td>
</tr>
) : (
sources.map((src) => (
<SourcePackagesList
key={src.url}
filter={filter}
disabled={loading}
source={src}
renderDescription={renderDescription}
loadSource={() => loadSource(src.url)}
selectedOnly={selectedOnly}
selectedPackages={selectedPackages[src.url] ?? []}
setSelectedPackages={(v) => setSelectedSourcePackages(src, v)}
editSource={() => setEditingSource(src)}
deleteSource={() => setDeletingSource(src)}
/>
))
)}
{sources.length > 0 ? (
<tr className="package-source">
<td colSpan={2}></td>
<td colSpan={3} onClick={() => setAddingSource(true)}>
<Button size="sm" variant="success">
<FormattedMessage id="package_manager.add_source" />
</Button>
</td>
</tr>
) : undefined}
</tbody>
<tbody>{renderBody()}</tbody>
</Table>
</div>
</>

View file

@ -4,9 +4,19 @@
.package-source {
font-weight: bold;
.source-collapse {
padding-left: 0;
padding-right: 0;
.btn {
color: $text-color;
}
}
.source-controls {
align-items: center;
display: flex;
gap: 0.5rem;
justify-content: end;
}
}
@ -16,10 +26,6 @@
cursor: pointer;
}
.package-collapse-button {
color: $text-color;
}
.package-manager-table-container {
max-height: 300px;
overflow-y: auto;
@ -31,39 +37,54 @@
top: 0;
z-index: 1;
.button-cell {
.check-cell {
width: 40px;
}
.collapse-cell {
width: 30px;
}
}
table td {
vertical-align: middle;
}
.package-name,
.package-id,
.package-version,
.package-date,
.package-name,
.package-id {
.package-latest-version,
.package-latest-date {
display: block;
}
.package-id,
.package-date,
.package-id {
.package-latest-date {
color: $muted-gray;
font-size: 0.8rem;
}
.package-id,
.package-version,
.package-date,
.package-latest-version,
.package-latest-date {
white-space: nowrap;
}
.package-update-available {
.package-latest-version,
.package-latest-date {
font-weight: 700;
}
}
.package-manager-toolbar {
display: flex;
justify-content: space-between;
div {
display: flex;
}
.btn {
margin-left: 0.5em;
}
gap: 0.5rem;
padding-bottom: 0.25rem;
}
.package-required-by {

View file

@ -1,4 +1,9 @@
import { ApolloCache, DocumentNode, FetchResult } from "@apollo/client";
import {
ApolloCache,
DocumentNode,
FetchResult,
useQuery,
} from "@apollo/client";
import { Modifiers } from "@apollo/client/cache";
import {
isField,
@ -1961,43 +1966,6 @@ export const mutateSubmitStashBoxPerformerDraft = (
variables: { input },
});
/// Packages
export const useInstalledScraperPackages = GQL.useInstalledScraperPackagesQuery;
export const useInstalledScraperPackagesStatus =
GQL.useInstalledScraperPackagesStatusQuery;
export const queryAvailableScraperPackages = (source: string) =>
client.query<GQL.AvailableScraperPackagesQuery>({
query: GQL.AvailableScraperPackagesDocument,
variables: {
source,
},
fetchPolicy: "network-only",
});
export const useInstallScraperPackages = GQL.useInstallScraperPackagesMutation;
export const useUpdateScraperPackages = GQL.useUpdateScraperPackagesMutation;
export const useUninstallScraperPackages =
GQL.useUninstallScraperPackagesMutation;
export const useInstalledPluginPackages = GQL.useInstalledPluginPackagesQuery;
export const useInstalledPluginPackagesStatus =
GQL.useInstalledPluginPackagesStatusQuery;
export const queryAvailablePluginPackages = (source: string) =>
client.query<GQL.AvailablePluginPackagesQuery>({
query: GQL.AvailablePluginPackagesDocument,
variables: {
source,
},
fetchPolicy: "network-only",
});
export const useInstallPluginPackages = GQL.useInstallPluginPackagesMutation;
export const useUpdatePluginPackages = GQL.useUpdatePluginPackagesMutation;
export const useUninstallPluginPackages =
GQL.useUninstallPluginPackagesMutation;
/// Configuration
export const useConfiguration = () => GQL.useConfigurationQuery();
@ -2043,6 +2011,65 @@ export const useJobsSubscribe = () => GQL.useJobsSubscribeSubscription();
export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
// all scraper-related queries
export const scraperMutationImpactedQueries = [
GQL.ListMovieScrapersDocument,
GQL.ListPerformerScrapersDocument,
GQL.ListSceneScrapersDocument,
GQL.InstalledScraperPackagesDocument,
GQL.InstalledScraperPackagesStatusDocument,
];
export const mutateReloadScrapers = () =>
client.mutate<GQL.ReloadScrapersMutation>({
mutation: GQL.ReloadScrapersDocument,
update(cache, result) {
if (!result.data?.reloadScrapers) return;
evictQueries(cache, scraperMutationImpactedQueries);
},
});
// all plugin-related queries
export const pluginMutationImpactedQueries = [
GQL.PluginsDocument,
GQL.PluginTasksDocument,
GQL.InstalledPluginPackagesDocument,
GQL.InstalledPluginPackagesStatusDocument,
];
export const mutateReloadPlugins = () =>
client.mutate<GQL.ReloadPluginsMutation>({
mutation: GQL.ReloadPluginsDocument,
update(cache, result) {
if (!result.data?.reloadPlugins) return;
evictQueries(cache, pluginMutationImpactedQueries);
},
});
type BoolMap = { [key: string]: boolean };
export const mutateSetPluginsEnabled = (enabledMap: BoolMap) =>
client.mutate<GQL.SetPluginsEnabledMutation>({
mutation: GQL.SetPluginsEnabledDocument,
variables: { enabledMap },
update(cache, result) {
if (!result.data?.setPluginsEnabled) return;
for (const id in enabledMap) {
cache.modify({
id: cache.identify({ __typename: "Plugin", id }),
fields: {
enabled() {
return enabledMap[id];
},
},
});
}
},
});
function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
if (!result.data) return;
@ -2051,7 +2078,15 @@ function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
export const useConfigureGeneral = () =>
GQL.useConfigureGeneralMutation({
update: updateConfiguration,
update(cache, result) {
if (!result.data?.configureGeneral) return;
evictQueries(cache, [
GQL.ConfigurationDocument,
...scraperMutationImpactedQueries,
...pluginMutationImpactedQueries,
]);
},
});
export const useConfigureInterface = () =>
@ -2097,48 +2132,6 @@ export const useAddTempDLNAIP = () => GQL.useAddTempDlnaipMutation();
export const useRemoveTempDLNAIP = () => GQL.useRemoveTempDlnaipMutation();
export const mutateReloadScrapers = () =>
client.mutate<GQL.ReloadScrapersMutation>({
mutation: GQL.ReloadScrapersDocument,
update(cache, result) {
if (!result.data?.reloadScrapers) return;
evictQueries(cache, [
GQL.ListMovieScrapersDocument,
GQL.ListPerformerScrapersDocument,
GQL.ListSceneScrapersDocument,
]);
},
});
const pluginMutationImpactedQueries = [
GQL.PluginsDocument,
GQL.PluginTasksDocument,
];
export const mutateReloadPlugins = () =>
client.mutate<GQL.ReloadPluginsMutation>({
mutation: GQL.ReloadPluginsDocument,
update(cache, result) {
if (!result.data?.reloadPlugins) return;
evictQueries(cache, pluginMutationImpactedQueries);
},
});
type BoolMap = { [key: string]: boolean };
export const mutateSetPluginsEnabled = (enabledMap: BoolMap) =>
client.mutate<GQL.SetPluginsEnabledMutation>({
mutation: GQL.SetPluginsEnabledDocument,
variables: { enabledMap },
update(cache, result) {
if (!result.data?.setPluginsEnabled) return;
evictQueries(cache, pluginMutationImpactedQueries);
},
});
export const mutateStopJob = (jobID: string) =>
client.mutate<GQL.StopJobMutation>({
mutation: GQL.StopJobDocument,
@ -2172,6 +2165,118 @@ export const mutateMigrate = (input: GQL.MigrateInput) =>
},
});
/// Packages
// Acts like GQL.useInstalledScraperPackagesStatusQuery if loadUpgrades is true,
// and GQL.useInstalledScraperPackagesQuery if it is false
export const useInstalledScraperPackages = <T extends boolean>(
loadUpgrades: T
) => {
const query = loadUpgrades
? GQL.InstalledScraperPackagesStatusDocument
: GQL.InstalledScraperPackagesDocument;
type TData = T extends true
? GQL.InstalledScraperPackagesStatusQuery
: GQL.InstalledScraperPackagesQuery;
type TVariables = T extends true
? GQL.InstalledScraperPackagesStatusQueryVariables
: GQL.InstalledScraperPackagesQueryVariables;
return useQuery<TData, TVariables>(query);
};
export const queryAvailableScraperPackages = (source: string) =>
client.query<GQL.AvailableScraperPackagesQuery>({
query: GQL.AvailableScraperPackagesDocument,
variables: {
source,
},
fetchPolicy: "network-only",
});
export const mutateInstallScraperPackages = (
packages: GQL.PackageSpecInput[]
) =>
client.mutate<GQL.InstallScraperPackagesMutation>({
mutation: GQL.InstallScraperPackagesDocument,
variables: {
packages,
},
});
export const mutateUpdateScraperPackages = (packages: GQL.PackageSpecInput[]) =>
client.mutate<GQL.UpdateScraperPackagesMutation>({
mutation: GQL.UpdateScraperPackagesDocument,
variables: {
packages,
},
});
export const mutateUninstallScraperPackages = (
packages: GQL.PackageSpecInput[]
) =>
client.mutate<GQL.UninstallScraperPackagesMutation>({
mutation: GQL.UninstallScraperPackagesDocument,
variables: {
packages,
},
});
// Acts like GQL.useInstalledPluginPackagesStatusQuery if loadUpgrades is true,
// and GQL.useInstalledPluginPackagesQuery if it is false
export const useInstalledPluginPackages = <T extends boolean>(
loadUpgrades: T
) => {
const query = loadUpgrades
? GQL.InstalledPluginPackagesStatusDocument
: GQL.InstalledPluginPackagesDocument;
type TData = T extends true
? GQL.InstalledPluginPackagesStatusQuery
: GQL.InstalledPluginPackagesQuery;
type TVariables = T extends true
? GQL.InstalledPluginPackagesStatusQueryVariables
: GQL.InstalledPluginPackagesQueryVariables;
return useQuery<TData, TVariables>(query);
};
export const queryAvailablePluginPackages = (source: string) =>
client.query<GQL.AvailablePluginPackagesQuery>({
query: GQL.AvailablePluginPackagesDocument,
variables: {
source,
},
fetchPolicy: "network-only",
});
export const mutateInstallPluginPackages = (packages: GQL.PackageSpecInput[]) =>
client.mutate<GQL.InstallPluginPackagesMutation>({
mutation: GQL.InstallPluginPackagesDocument,
variables: {
packages,
},
});
export const mutateUpdatePluginPackages = (packages: GQL.PackageSpecInput[]) =>
client.mutate<GQL.UpdatePluginPackagesMutation>({
mutation: GQL.UpdatePluginPackagesDocument,
variables: {
packages,
},
});
export const mutateUninstallPluginPackages = (
packages: GQL.PackageSpecInput[]
) =>
client.mutate<GQL.UninstallPluginPackagesMutation>({
mutation: GQL.UninstallPluginPackagesDocument,
variables: {
packages,
},
});
/// Tasks
export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>

View file

@ -360,6 +360,10 @@
"number_of_parallel_task_for_scan_generation_desc": "Set to 0 for auto-detection. Warning running more tasks than is required to achieve 100% cpu utilisation will decrease performance and potentially cause other issues.",
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
"parallel_scan_head": "Parallel Scan/Generation",
"plugins_path": {
"description": "Directory location of plugin configuration files",
"heading": "Plugins Path"
},
"preview_generation": "Preview Generation",
"python_path": {
"description": "Path to the python executable (not just the folder). Used for script scrapers and plugins. If blank, python will be resolved from the environment",
@ -1098,6 +1102,7 @@
"selected_only": "Selected only",
"show_all": "Show all",
"uninstall": "Uninstall",
"unknown": "<unknown>",
"update": "Update",
"version": "Version"
},