mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
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:
parent
23b4d4f1e0
commit
a1bd7cf817
16 changed files with 611 additions and 500 deletions
|
|
@ -9,6 +9,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||||
generatedPath
|
generatedPath
|
||||||
metadataPath
|
metadataPath
|
||||||
scrapersPath
|
scrapersPath
|
||||||
|
pluginsPath
|
||||||
cachePath
|
cachePath
|
||||||
blobsPath
|
blobsPath
|
||||||
blobsStorage
|
blobsStorage
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ query InstalledPluginPackages {
|
||||||
query InstalledPluginPackagesStatus {
|
query InstalledPluginPackagesStatus {
|
||||||
installedPackages(type: Plugin) {
|
installedPackages(type: Plugin) {
|
||||||
...PackageData
|
...PackageData
|
||||||
upgrade {
|
source_package {
|
||||||
...PackageData
|
...PackageData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ query InstalledScraperPackages {
|
||||||
query InstalledScraperPackagesStatus {
|
query InstalledScraperPackagesStatus {
|
||||||
installedPackages(type: Scraper) {
|
installedPackages(type: Scraper) {
|
||||||
...PackageData
|
...PackageData
|
||||||
upgrade {
|
source_package {
|
||||||
...PackageData
|
...PackageData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ input ConfigGeneralInput {
|
||||||
metadataPath: String
|
metadataPath: String
|
||||||
"Path to scrapers"
|
"Path to scrapers"
|
||||||
scrapersPath: String
|
scrapersPath: String
|
||||||
|
"Path to plugins"
|
||||||
|
pluginsPath: String
|
||||||
"Path to cache"
|
"Path to cache"
|
||||||
cachePath: String
|
cachePath: String
|
||||||
"Path to blobs - required for filesystem blob storage"
|
"Path to blobs - required for filesystem blob storage"
|
||||||
|
|
@ -189,6 +191,8 @@ type ConfigGeneralResult {
|
||||||
configFilePath: String!
|
configFilePath: String!
|
||||||
"Path to scrapers"
|
"Path to scrapers"
|
||||||
scrapersPath: String!
|
scrapersPath: String!
|
||||||
|
"Path to plugins"
|
||||||
|
pluginsPath: String!
|
||||||
"Path to cache"
|
"Path to cache"
|
||||||
cachePath: String!
|
cachePath: String!
|
||||||
"Path to blobs - required for filesystem blob storage"
|
"Path to blobs - required for filesystem blob storage"
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ type Package {
|
||||||
|
|
||||||
sourceURL: String!
|
sourceURL: String!
|
||||||
|
|
||||||
"The available upgraded version of this package"
|
"The version of this package currently available from the remote source"
|
||||||
upgrade: Package
|
source_package: Package
|
||||||
|
|
||||||
metadata: Map!
|
metadata: Map!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshScraperCache := false
|
refreshScraperCache := false
|
||||||
|
refreshScraperSource := false
|
||||||
existingScrapersPath := c.GetScrapersPath()
|
existingScrapersPath := c.GetScrapersPath()
|
||||||
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
|
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
|
||||||
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
|
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
|
refreshScraperCache = true
|
||||||
|
refreshScraperSource = true
|
||||||
c.Set(config.ScrapersPath, input.ScrapersPath)
|
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()
|
existingMetadataPath := c.GetMetadataPath()
|
||||||
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
|
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
|
||||||
if err := validateDir(config.Metadata, *input.MetadataPath, true); err != nil {
|
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)
|
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshScraperSource := false
|
|
||||||
if input.ScraperPackageSources != nil {
|
if input.ScraperPackageSources != nil {
|
||||||
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
|
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||||
refreshScraperSource = true
|
refreshScraperSource = true
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshPluginSource := false
|
|
||||||
if input.PluginPackageSources != nil {
|
if input.PluginPackageSources != nil {
|
||||||
c.Set(config.PluginPackageSources, input.PluginPackageSources)
|
c.Set(config.PluginPackageSources, input.PluginPackageSources)
|
||||||
refreshPluginSource = true
|
refreshPluginSource = true
|
||||||
|
|
@ -367,6 +380,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
||||||
if refreshScraperCache {
|
if refreshScraperCache {
|
||||||
manager.GetInstance().RefreshScraperCache()
|
manager.GetInstance().RefreshScraperCache()
|
||||||
}
|
}
|
||||||
|
if refreshPluginCache {
|
||||||
|
manager.GetInstance().RefreshPluginCache()
|
||||||
|
}
|
||||||
if refreshStreamManager {
|
if refreshStreamManager {
|
||||||
manager.GetInstance().RefreshStreamManager()
|
manager.GetInstance().RefreshStreamManager()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
||||||
MetadataPath: config.GetMetadataPath(),
|
MetadataPath: config.GetMetadataPath(),
|
||||||
ConfigFilePath: config.GetConfigFile(),
|
ConfigFilePath: config.GetConfigFile(),
|
||||||
ScrapersPath: config.GetScrapersPath(),
|
ScrapersPath: config.GetScrapersPath(),
|
||||||
|
PluginsPath: config.GetPluginsPath(),
|
||||||
CachePath: config.GetCachePath(),
|
CachePath: config.GetCachePath(),
|
||||||
BlobsPath: config.GetBlobsPath(),
|
BlobsPath: config.GetBlobsPath(),
|
||||||
BlobsStorage: config.GetBlobsStorage(),
|
BlobsStorage: config.GetBlobsStorage(),
|
||||||
|
|
|
||||||
|
|
@ -98,11 +98,24 @@ func sortedPackageSpecKeys[V any](m map[models.PackageSpecInput]V) []models.Pack
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
if strings.EqualFold(keys[i].ID, keys[j].ID) {
|
a := keys[i]
|
||||||
return keys[i].ID < keys[j].ID
|
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
|
return keys
|
||||||
|
|
@ -129,9 +142,9 @@ func (r *queryResolver) getInstalledPackagesWithUpgrades(ctx context.Context, pm
|
||||||
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
|
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
|
||||||
v := packageStatusIndex[k]
|
v := packageStatusIndex[k]
|
||||||
p := manifestToPackage(*v.Local)
|
p := manifestToPackage(*v.Local)
|
||||||
if v.Upgradable() {
|
if v.Remote != nil {
|
||||||
pp := remotePackageToPackage(*v.Remote, allRemoteList)
|
pp := remotePackageToPackage(*v.Remote, allRemoteList)
|
||||||
p.Upgrade = pp
|
p.SourcePackage = pp
|
||||||
}
|
}
|
||||||
ret[i] = p
|
ret[i] = p
|
||||||
i++
|
i++
|
||||||
|
|
@ -146,19 +159,19 @@ func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageTy
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
installed, err := pm.ListInstalled(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret []*Package
|
var ret []*Package
|
||||||
|
|
||||||
if sliceutil.Contains(graphql.CollectAllFields(ctx), "upgrade") {
|
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
|
||||||
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
|
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
installed, err := pm.ListInstalled(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ret = make([]*Package, len(installed))
|
ret = make([]*Package, len(installed))
|
||||||
i := 0
|
i := 0
|
||||||
for _, k := range sortedPackageSpecKeys(installed) {
|
for _, k := range sortedPackageSpecKeys(installed) {
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ const (
|
||||||
PluginsSettingPrefix = PluginsSetting + "."
|
PluginsSettingPrefix = PluginsSetting + "."
|
||||||
DisabledPlugins = "plugins.disabled"
|
DisabledPlugins = "plugins.disabled"
|
||||||
|
|
||||||
sourceDefaultPath = "stable"
|
sourceDefaultPath = "community"
|
||||||
sourceDefaultName = "Community (stable)"
|
sourceDefaultName = "Community (stable)"
|
||||||
|
|
||||||
PluginPackageSources = "plugins.package_sources"
|
PluginPackageSources = "plugins.package_sources"
|
||||||
|
|
@ -1666,16 +1666,16 @@ func (i *Config) setDefaultValues() {
|
||||||
i.main.SetDefault(NoProxy, noProxyDefault)
|
i.main.SetDefault(NoProxy, noProxyDefault)
|
||||||
|
|
||||||
// set default package sources
|
// set default package sources
|
||||||
i.main.SetDefault(PluginPackageSources, map[string]string{
|
i.main.SetDefault(PluginPackageSources, []map[string]string{{
|
||||||
"name": sourceDefaultName,
|
"name": sourceDefaultName,
|
||||||
"url": pluginPackageSourcesDefault,
|
"url": pluginPackageSourcesDefault,
|
||||||
"local_path": sourceDefaultPath,
|
"localpath": sourceDefaultPath,
|
||||||
})
|
}})
|
||||||
i.main.SetDefault(ScraperPackageSources, map[string]string{
|
i.main.SetDefault(ScraperPackageSources, []map[string]string{{
|
||||||
"name": sourceDefaultName,
|
"name": sourceDefaultName,
|
||||||
"url": scraperPackageSourcesDefault,
|
"url": scraperPackageSourcesDefault,
|
||||||
"local_path": sourceDefaultPath,
|
"localpath": sourceDefaultPath,
|
||||||
})
|
}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// setExistingSystemDefaults sets config options that are new and unset in an existing install,
|
// setExistingSystemDefaults sets config options that are new and unset in an existing install,
|
||||||
|
|
|
||||||
|
|
@ -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 * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
evictQueries,
|
evictQueries,
|
||||||
getClient,
|
getClient,
|
||||||
queryAvailablePluginPackages,
|
queryAvailablePluginPackages,
|
||||||
useInstallPluginPackages,
|
|
||||||
useInstalledPluginPackages,
|
useInstalledPluginPackages,
|
||||||
useInstalledPluginPackagesStatus,
|
mutateInstallPluginPackages,
|
||||||
useUninstallPluginPackages,
|
mutateUninstallPluginPackages,
|
||||||
useUpdatePluginPackages,
|
mutateUpdatePluginPackages,
|
||||||
|
pluginMutationImpactedQueries,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useMonitorJob } from "src/utils/job";
|
import { useMonitorJob } from "src/utils/job";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,95 +20,59 @@ import { useSettings } from "./context";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { SettingSection } from "./SettingSection";
|
import { SettingSection } from "./SettingSection";
|
||||||
|
|
||||||
const impactedPackageChangeQueries = [
|
|
||||||
GQL.PluginsDocument,
|
|
||||||
GQL.PluginTasksDocument,
|
|
||||||
GQL.InstalledPluginPackagesDocument,
|
|
||||||
GQL.InstalledPluginPackagesStatusDocument,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const InstalledPluginPackages: React.FC = () => {
|
export const InstalledPluginPackages: React.FC = () => {
|
||||||
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
||||||
const [jobID, setJobID] = useState<string>();
|
const [jobID, setJobID] = useState<string>();
|
||||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||||
|
|
||||||
const { data: installedPlugins, refetch: refetchPackages1 } =
|
const { data, previousData, refetch, loading, error } =
|
||||||
useInstalledPluginPackages({
|
useInstalledPluginPackages(loadUpgrades);
|
||||||
skip: loadUpgrades,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: withStatus,
|
|
||||||
refetch: refetchPackages2,
|
|
||||||
loading: statusLoading,
|
|
||||||
} = useInstalledPluginPackagesStatus({
|
|
||||||
skip: !loadUpgrades,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [updatePackages] = useUpdatePluginPackages();
|
|
||||||
const [uninstallPackages] = useUninstallPluginPackages();
|
|
||||||
|
|
||||||
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await updatePackages({
|
const r = await mutateUpdatePluginPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.updatePackages);
|
setJobID(r.data?.updatePackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await uninstallPackages({
|
const r = await mutateUninstallPluginPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.uninstallPackages);
|
setJobID(r.data?.uninstallPackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refetchPackages() {
|
|
||||||
refetchPackages1();
|
|
||||||
refetchPackages2();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPackageChanges() {
|
function onPackageChanges() {
|
||||||
// job is complete, refresh all local data
|
// job is complete, refresh all local data
|
||||||
const ac = getClient();
|
const ac = getClient();
|
||||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
evictQueries(ac.cache, pluginMutationImpactedQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCheckForUpdates() {
|
function onCheckForUpdates() {
|
||||||
if (!loadUpgrades) {
|
if (!loadUpgrades) {
|
||||||
setLoadUpgrades(true);
|
setLoadUpgrades(true);
|
||||||
} else {
|
} else {
|
||||||
refetchPackages();
|
refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackages = useMemo(() => {
|
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
|
||||||
if (withStatus?.installedPackages) {
|
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
|
||||||
return withStatus.installedPackages;
|
// to prevent displaying a "No packages found" message
|
||||||
}
|
const installedPackages =
|
||||||
|
data?.installedPackages ?? previousData?.installedPackages ?? [];
|
||||||
return installedPlugins?.installedPackages ?? [];
|
|
||||||
}, [installedPlugins, withStatus]);
|
|
||||||
|
|
||||||
const loading = !!job || statusLoading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingSection headingID="config.plugins.installed_plugins">
|
<SettingSection headingID="config.plugins.installed_plugins">
|
||||||
<div className="package-manager">
|
<div className="package-manager">
|
||||||
<InstalledPackages
|
<InstalledPackages
|
||||||
loading={loading}
|
loading={!!job || loading}
|
||||||
|
error={error?.message}
|
||||||
packages={installedPackages}
|
packages={installedPackages}
|
||||||
onCheckForUpdates={onCheckForUpdates}
|
onCheckForUpdates={onCheckForUpdates}
|
||||||
onUpdatePackages={(packages) =>
|
onUpdatePackages={(packages) =>
|
||||||
onUpdatePackages(
|
onUpdatePackages(
|
||||||
packages.map((p) => ({
|
packages.map((p) => ({
|
||||||
id: p.package_id,
|
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>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
@ -130,18 +94,11 @@ export const InstalledPluginPackages: React.FC = () => {
|
||||||
export const AvailablePluginPackages: React.FC = () => {
|
export const AvailablePluginPackages: React.FC = () => {
|
||||||
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
||||||
|
|
||||||
const [sources, setSources] = useState<GQL.PackageSource[]>();
|
|
||||||
const [jobID, setJobID] = useState<string>();
|
const [jobID, setJobID] = useState<string>();
|
||||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||||
|
|
||||||
const [installPackages] = useInstallPluginPackages();
|
|
||||||
|
|
||||||
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await installPackages({
|
const r = await mutateInstallPluginPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.installPackages);
|
setJobID(r.data?.installPackages);
|
||||||
}
|
}
|
||||||
|
|
@ -149,15 +106,9 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
function onPackageChanges() {
|
function onPackageChanges() {
|
||||||
// job is complete, refresh all local data
|
// job is complete, refresh all local data
|
||||||
const ac = getClient();
|
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[]> {
|
async function loadSource(source: string): Promise<RemotePackage[]> {
|
||||||
const { data } = await queryAvailablePluginPackages(source);
|
const { data } = await queryAvailablePluginPackages(source);
|
||||||
return data.availablePackages;
|
return data.availablePackages;
|
||||||
|
|
@ -167,10 +118,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
saveGeneral({
|
saveGeneral({
|
||||||
pluginPackageSources: [...(general.pluginPackageSources ?? []), source],
|
pluginPackageSources: [...(general.pluginPackageSources ?? []), source],
|
||||||
});
|
});
|
||||||
|
|
||||||
setSources((prev) => {
|
|
||||||
return [...(prev ?? []), source];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
||||||
|
|
@ -179,10 +126,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
s.url === existing.url ? changed : s
|
s.url === existing.url ? changed : s
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSources((prev) => {
|
|
||||||
return prev?.map((s) => (s.url === existing.url ? changed : s));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSource(source: GQL.PackageSource) {
|
function deleteSource(source: GQL.PackageSource) {
|
||||||
|
|
@ -191,10 +134,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
(s) => s.url !== source.url
|
(s) => s.url !== source.url
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSources((prev) => {
|
|
||||||
return prev?.filter((s) => s.url !== source.url);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDescription(pkg: RemotePackage) {
|
function renderDescription(pkg: RemotePackage) {
|
||||||
|
|
@ -208,6 +147,8 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
|
|
||||||
const loading = !!job;
|
const loading = !!job;
|
||||||
|
|
||||||
|
const sources = general?.pluginPackageSources ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingSection headingID="config.plugins.available_plugins">
|
<SettingSection headingID="config.plugins.available_plugins">
|
||||||
<div className="package-manager">
|
<div className="package-manager">
|
||||||
|
|
@ -216,7 +157,7 @@ export const AvailablePluginPackages: React.FC = () => {
|
||||||
onInstallPackages={onInstallPackages}
|
onInstallPackages={onInstallPackages}
|
||||||
renderDescription={renderDescription}
|
renderDescription={renderDescription}
|
||||||
loadSource={(source) => loadSource(source)}
|
loadSource={(source) => loadSource(source)}
|
||||||
sources={sources ?? []}
|
sources={sources}
|
||||||
addSource={addSource}
|
addSource={addSource}
|
||||||
editSource={editSource}
|
editSource={editSource}
|
||||||
deleteSource={deleteSource}
|
deleteSource={deleteSource}
|
||||||
|
|
|
||||||
|
|
@ -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 * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
evictQueries,
|
evictQueries,
|
||||||
getClient,
|
getClient,
|
||||||
queryAvailableScraperPackages,
|
queryAvailableScraperPackages,
|
||||||
useInstallScraperPackages,
|
|
||||||
useInstalledScraperPackages,
|
useInstalledScraperPackages,
|
||||||
useInstalledScraperPackagesStatus,
|
mutateUpdateScraperPackages,
|
||||||
useUninstallScraperPackages,
|
mutateUninstallScraperPackages,
|
||||||
useUpdateScraperPackages,
|
mutateInstallScraperPackages,
|
||||||
|
scraperMutationImpactedQueries,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useMonitorJob } from "src/utils/job";
|
import { useMonitorJob } from "src/utils/job";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,96 +20,59 @@ import { useSettings } from "./context";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { SettingSection } from "./SettingSection";
|
import { SettingSection } from "./SettingSection";
|
||||||
|
|
||||||
const impactedPackageChangeQueries = [
|
|
||||||
GQL.ListPerformerScrapersDocument,
|
|
||||||
GQL.ListSceneScrapersDocument,
|
|
||||||
GQL.ListMovieScrapersDocument,
|
|
||||||
GQL.InstalledScraperPackagesDocument,
|
|
||||||
GQL.InstalledScraperPackagesStatusDocument,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const InstalledScraperPackages: React.FC = () => {
|
export const InstalledScraperPackages: React.FC = () => {
|
||||||
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
||||||
const [jobID, setJobID] = useState<string>();
|
const [jobID, setJobID] = useState<string>();
|
||||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||||
|
|
||||||
const { data: installedScrapers, refetch: refetchPackages1 } =
|
const { data, previousData, refetch, loading, error } =
|
||||||
useInstalledScraperPackages({
|
useInstalledScraperPackages(loadUpgrades);
|
||||||
skip: loadUpgrades,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: withStatus,
|
|
||||||
refetch: refetchPackages2,
|
|
||||||
loading: statusLoading,
|
|
||||||
} = useInstalledScraperPackagesStatus({
|
|
||||||
skip: !loadUpgrades,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [updatePackages] = useUpdateScraperPackages();
|
|
||||||
const [uninstallPackages] = useUninstallScraperPackages();
|
|
||||||
|
|
||||||
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await updatePackages({
|
const r = await mutateUpdateScraperPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.updatePackages);
|
setJobID(r.data?.updatePackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await uninstallPackages({
|
const r = await mutateUninstallScraperPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.uninstallPackages);
|
setJobID(r.data?.uninstallPackages);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refetchPackages() {
|
|
||||||
refetchPackages1();
|
|
||||||
refetchPackages2();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPackageChanges() {
|
function onPackageChanges() {
|
||||||
// job is complete, refresh all local data
|
// job is complete, refresh all local data
|
||||||
const ac = getClient();
|
const ac = getClient();
|
||||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
evictQueries(ac.cache, scraperMutationImpactedQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCheckForUpdates() {
|
function onCheckForUpdates() {
|
||||||
if (!loadUpgrades) {
|
if (!loadUpgrades) {
|
||||||
setLoadUpgrades(true);
|
setLoadUpgrades(true);
|
||||||
} else {
|
} else {
|
||||||
refetchPackages();
|
refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackages = useMemo(() => {
|
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
|
||||||
if (withStatus?.installedPackages) {
|
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
|
||||||
return withStatus.installedPackages;
|
// to prevent displaying a "No packages found" message
|
||||||
}
|
const installedPackages =
|
||||||
|
data?.installedPackages ?? previousData?.installedPackages ?? [];
|
||||||
return installedScrapers?.installedPackages ?? [];
|
|
||||||
}, [installedScrapers, withStatus]);
|
|
||||||
|
|
||||||
const loading = !!job || statusLoading;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingSection headingID="config.scraping.installed_scrapers">
|
<SettingSection headingID="config.scraping.installed_scrapers">
|
||||||
<div className="package-manager">
|
<div className="package-manager">
|
||||||
<InstalledPackages
|
<InstalledPackages
|
||||||
loading={loading}
|
loading={!!job || loading}
|
||||||
|
error={error?.message}
|
||||||
packages={installedPackages}
|
packages={installedPackages}
|
||||||
onCheckForUpdates={onCheckForUpdates}
|
onCheckForUpdates={onCheckForUpdates}
|
||||||
onUpdatePackages={(packages) =>
|
onUpdatePackages={(packages) =>
|
||||||
onUpdatePackages(
|
onUpdatePackages(
|
||||||
packages.map((p) => ({
|
packages.map((p) => ({
|
||||||
id: p.package_id,
|
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>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
@ -131,18 +94,11 @@ export const InstalledScraperPackages: React.FC = () => {
|
||||||
export const AvailableScraperPackages: React.FC = () => {
|
export const AvailableScraperPackages: React.FC = () => {
|
||||||
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
||||||
|
|
||||||
const [sources, setSources] = useState<GQL.PackageSource[]>();
|
|
||||||
const [jobID, setJobID] = useState<string>();
|
const [jobID, setJobID] = useState<string>();
|
||||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||||
|
|
||||||
const [installPackages] = useInstallScraperPackages();
|
|
||||||
|
|
||||||
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||||
const r = await installPackages({
|
const r = await mutateInstallScraperPackages(packages);
|
||||||
variables: {
|
|
||||||
packages,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setJobID(r.data?.installPackages);
|
setJobID(r.data?.installPackages);
|
||||||
}
|
}
|
||||||
|
|
@ -150,15 +106,9 @@ export const AvailableScraperPackages: React.FC = () => {
|
||||||
function onPackageChanges() {
|
function onPackageChanges() {
|
||||||
// job is complete, refresh all local data
|
// job is complete, refresh all local data
|
||||||
const ac = getClient();
|
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[]> {
|
async function loadSource(source: string): Promise<RemotePackage[]> {
|
||||||
const { data } = await queryAvailableScraperPackages(source);
|
const { data } = await queryAvailableScraperPackages(source);
|
||||||
return data.availablePackages;
|
return data.availablePackages;
|
||||||
|
|
@ -168,10 +118,6 @@ export const AvailableScraperPackages: React.FC = () => {
|
||||||
saveGeneral({
|
saveGeneral({
|
||||||
scraperPackageSources: [...(general.scraperPackageSources ?? []), source],
|
scraperPackageSources: [...(general.scraperPackageSources ?? []), source],
|
||||||
});
|
});
|
||||||
|
|
||||||
setSources((prev) => {
|
|
||||||
return [...(prev ?? []), source];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
||||||
|
|
@ -180,10 +126,6 @@ export const AvailableScraperPackages: React.FC = () => {
|
||||||
s.url === existing.url ? changed : s
|
s.url === existing.url ? changed : s
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
setSources((prev) => {
|
|
||||||
return prev?.map((s) => (s.url === existing.url ? changed : s));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSource(source: GQL.PackageSource) {
|
function deleteSource(source: GQL.PackageSource) {
|
||||||
|
|
@ -192,10 +134,12 @@ export const AvailableScraperPackages: React.FC = () => {
|
||||||
(s) => s.url !== source.url
|
(s) => s.url !== source.url
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setSources((prev) => {
|
function renderDescription(pkg: RemotePackage) {
|
||||||
return prev?.filter((s) => s.url !== source.url);
|
if (pkg.metadata.description) {
|
||||||
});
|
return pkg.metadata.description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) return <h1>{error.message}</h1>;
|
if (error) return <h1>{error.message}</h1>;
|
||||||
|
|
@ -203,14 +147,17 @@ export const AvailableScraperPackages: React.FC = () => {
|
||||||
|
|
||||||
const loading = !!job;
|
const loading = !!job;
|
||||||
|
|
||||||
|
const sources = general?.scraperPackageSources ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingSection headingID="config.scraping.available_scrapers">
|
<SettingSection headingID="config.scraping.available_scrapers">
|
||||||
<div className="package-manager">
|
<div className="package-manager">
|
||||||
<AvailablePackages
|
<AvailablePackages
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onInstallPackages={onInstallPackages}
|
onInstallPackages={onInstallPackages}
|
||||||
|
renderDescription={renderDescription}
|
||||||
loadSource={(source) => loadSource(source)}
|
loadSource={(source) => loadSource(source)}
|
||||||
sources={sources ?? []}
|
sources={sources}
|
||||||
addSource={addSource}
|
addSource={addSource}
|
||||||
editSource={editSource}
|
editSource={editSource}
|
||||||
deleteSource={deleteSource}
|
deleteSource={deleteSource}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||||
onChange={(v) => saveGeneral({ scrapersPath: v })}
|
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
|
<StringSetting
|
||||||
id="metadata-path"
|
id="metadata-path"
|
||||||
headingID="config.general.metadata_path.heading"
|
headingID="config.general.metadata_path.heading"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Icon } from "../Icon";
|
import { Icon } from "../Icon";
|
||||||
|
import cx from "classnames";
|
||||||
import {
|
import {
|
||||||
|
faAnglesUp,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faRotate,
|
faRotate,
|
||||||
|
|
@ -17,7 +19,19 @@ import { LoadingIndicator } from "../LoadingIndicator";
|
||||||
import { ApolloError } from "@apollo/client";
|
import { ApolloError } from "@apollo/client";
|
||||||
import { ClearableInput } from "../ClearableInput";
|
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;
|
if (!date) return;
|
||||||
|
|
||||||
const d = new Date(date);
|
const d = new Date(date);
|
||||||
|
|
@ -59,14 +73,17 @@ const InstalledPackageRow: React.FC<{
|
||||||
}> = ({ loading, pkg, selected, togglePackage, updatesLoaded }) => {
|
}> = ({ loading, pkg, selected, togglePackage, updatesLoaded }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
function rowClassname() {
|
const updateAvailable = useMemo(() => {
|
||||||
if (pkg.upgrade?.version) {
|
if (!updatesLoaded) return false;
|
||||||
return "package-update-available";
|
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 (
|
return (
|
||||||
<tr className={rowClassname()}>
|
<tr className={cx({ "package-update-available": updateAvailable })}>
|
||||||
<td>
|
<td>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={selected}
|
checked={selected}
|
||||||
|
|
@ -79,17 +96,22 @@ const InstalledPackageRow: React.FC<{
|
||||||
<span className="package-id">{pkg.package_id}</span>
|
<span className="package-id">{pkg.package_id}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="package-version">{pkg.version}</span>
|
<span className="package-version">
|
||||||
<span className="package-date">{formatDate(intl, pkg.date)}</span>
|
{displayVersion(intl, pkg.version)}
|
||||||
|
</span>
|
||||||
|
<span className="package-date">{displayDate(intl, pkg.date)}</span>
|
||||||
</td>
|
</td>
|
||||||
{updatesLoaded ? (
|
{updatesLoaded && pkg.source_package && (
|
||||||
<td>
|
<td>
|
||||||
<span className="package-version">{pkg.upgrade?.version}</span>
|
<span className="package-latest-version">
|
||||||
<span className="package-date">
|
{displayVersion(intl, pkg.source_package.version)}
|
||||||
{formatDate(intl, pkg.upgrade?.date)}
|
{updateAvailable && <Icon icon={faAnglesUp} />}
|
||||||
|
</span>
|
||||||
|
<span className="package-latest-date">
|
||||||
|
{displayDate(intl, pkg.source_package.date)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
) : undefined}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -97,6 +119,7 @@ const InstalledPackageRow: React.FC<{
|
||||||
const InstalledPackagesList: React.FC<{
|
const InstalledPackagesList: React.FC<{
|
||||||
filter: string;
|
filter: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
error?: string;
|
||||||
updatesLoaded: boolean;
|
updatesLoaded: boolean;
|
||||||
packages: InstalledPackage[];
|
packages: InstalledPackage[];
|
||||||
checkedPackages: InstalledPackage[];
|
checkedPackages: InstalledPackage[];
|
||||||
|
|
@ -108,12 +131,13 @@ const InstalledPackagesList: React.FC<{
|
||||||
setCheckedPackages,
|
setCheckedPackages,
|
||||||
updatesLoaded,
|
updatesLoaded,
|
||||||
loading,
|
loading,
|
||||||
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
const checkedMap = useMemo(() => {
|
const checkedMap = useMemo(() => {
|
||||||
const map: Record<string, boolean> = {};
|
const map: Record<string, boolean> = {};
|
||||||
checkedPackages.forEach((pkg) => {
|
for (const pkg of checkedPackages) {
|
||||||
map[`${pkg.sourceURL}-${pkg.package_id}`] = true;
|
map[packageKey(pkg)] = true;
|
||||||
});
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [checkedPackages]);
|
}, [checkedPackages]);
|
||||||
|
|
||||||
|
|
@ -134,19 +158,54 @@ const InstalledPackagesList: React.FC<{
|
||||||
|
|
||||||
setCheckedPackages((prev) => {
|
setCheckedPackages((prev) => {
|
||||||
if (prev.includes(pkg)) {
|
if (prev.includes(pkg)) {
|
||||||
return prev.filter((n) => n.package_id !== pkg.package_id);
|
return prev.filter((n) => packageKey(n) !== packageKey(pkg));
|
||||||
} else {
|
} 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 (
|
return (
|
||||||
<div className="package-manager-table-container">
|
<div className="package-manager-table-container">
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="button-cell">
|
<th className="check-cell">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={allChecked ?? false}
|
checked={allChecked ?? false}
|
||||||
onChange={toggleAllChecked}
|
onChange={toggleAllChecked}
|
||||||
|
|
@ -166,28 +225,7 @@ const InstalledPackagesList: React.FC<{
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>{renderBody()}</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>
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -213,14 +251,12 @@ const InstalledPackagesToolbar: React.FC<{
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<div className="package-manager-toolbar">
|
<div className="package-manager-toolbar">
|
||||||
<div>
|
|
||||||
<ClearableInput
|
<ClearableInput
|
||||||
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
||||||
value={filter}
|
value={filter}
|
||||||
setValue={(v) => setFilter(v)}
|
setValue={(v) => setFilter(v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="flex-grow-1" />
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => onCheckForUpdates()}
|
onClick={() => onCheckForUpdates()}
|
||||||
|
|
@ -243,12 +279,12 @@ const InstalledPackagesToolbar: React.FC<{
|
||||||
<FormattedMessage id="package_manager.uninstall" />
|
<FormattedMessage id="package_manager.uninstall" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstalledPackages: React.FC<{
|
export const InstalledPackages: React.FC<{
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
error?: string;
|
||||||
packages: InstalledPackage[];
|
packages: InstalledPackage[];
|
||||||
updatesLoaded: boolean;
|
updatesLoaded: boolean;
|
||||||
onCheckForUpdates: () => void;
|
onCheckForUpdates: () => void;
|
||||||
|
|
@ -261,6 +297,7 @@ export const InstalledPackages: React.FC<{
|
||||||
onUpdatePackages,
|
onUpdatePackages,
|
||||||
onUninstallPackages,
|
onUninstallPackages,
|
||||||
loading,
|
loading,
|
||||||
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
const [checkedPackages, setCheckedPackages] = useState<InstalledPackage[]>(
|
const [checkedPackages, setCheckedPackages] = useState<InstalledPackage[]>(
|
||||||
[]
|
[]
|
||||||
|
|
@ -275,7 +312,7 @@ export const InstalledPackages: React.FC<{
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCheckedPackages((prev) => {
|
setCheckedPackages((prev) => {
|
||||||
const newVal = prev.filter((pkg) =>
|
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) {
|
if (newVal.length !== prev.length) {
|
||||||
return newVal;
|
return newVal;
|
||||||
|
|
@ -316,6 +353,7 @@ export const InstalledPackages: React.FC<{
|
||||||
<InstalledPackagesList
|
<InstalledPackagesList
|
||||||
filter={filter}
|
filter={filter}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
error={error}
|
||||||
packages={packages}
|
packages={packages}
|
||||||
// use original checked packages so that check boxes are not affected by filter
|
// use original checked packages so that check boxes are not affected by filter
|
||||||
checkedPackages={checkedPackages}
|
checkedPackages={checkedPackages}
|
||||||
|
|
@ -353,7 +391,6 @@ const AvailablePackagesToolbar: React.FC<{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="package-manager-toolbar">
|
<div className="package-manager-toolbar">
|
||||||
<div>
|
|
||||||
<ClearableInput
|
<ClearableInput
|
||||||
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
||||||
value={filter}
|
value={filter}
|
||||||
|
|
@ -368,8 +405,7 @@ const AvailablePackagesToolbar: React.FC<{
|
||||||
<FormattedMessage id={selectedOnlyId} />
|
<FormattedMessage id={selectedOnlyId} />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="flex-grow-1" />
|
||||||
<div>
|
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!hasSelectedPackages || loading}
|
disabled={!hasSelectedPackages || loading}
|
||||||
|
|
@ -378,7 +414,6 @@ const AvailablePackagesToolbar: React.FC<{
|
||||||
<FormattedMessage id="package_manager.install" />
|
<FormattedMessage id="package_manager.install" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -552,7 +587,7 @@ const AvailablePackageRow: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={pkg.package_id}>
|
<tr>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={selected ?? false}
|
checked={selected ?? false}
|
||||||
|
|
@ -565,8 +600,10 @@ const AvailablePackageRow: React.FC<{
|
||||||
<span className="package-id">{pkg.package_id}</span>
|
<span className="package-id">{pkg.package_id}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="package-version">{pkg.version}</span>
|
<span className="package-version">
|
||||||
<span className="package-date">{formatDate(intl, pkg.date)}</span>
|
{displayVersion(intl, pkg.version)}
|
||||||
|
</span>
|
||||||
|
<span className="package-date">{displayDate(intl, pkg.date)}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{renderRequiredBy()}
|
{renderRequiredBy()}
|
||||||
|
|
@ -655,36 +692,62 @@ const SourcePackagesList: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSourceOpen() {
|
function toggleSourceOpen() {
|
||||||
|
if (sourceOpen) {
|
||||||
|
setLoadError(undefined);
|
||||||
|
setSourceOpen(false);
|
||||||
|
} else {
|
||||||
if (packages === undefined) {
|
if (packages === undefined) {
|
||||||
loadPackages();
|
loadPackages();
|
||||||
}
|
}
|
||||||
|
setSourceOpen(true);
|
||||||
setSourceOpen((prev) => !prev);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCollapseButton() {
|
function renderContents() {
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<tr>
|
||||||
variant="minimal"
|
<td colSpan={2}></td>
|
||||||
size="sm"
|
<td colSpan={3}>
|
||||||
className="package-collapse-button"
|
<LoadingIndicator inline small />
|
||||||
onClick={() => toggleSourceOpen()}
|
</td>
|
||||||
>
|
</tr>
|
||||||
<Icon icon={sourceOpen ? faChevronDown : faChevronRight} />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = useMemo(() => {
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function getRequiredPackages(pkg: RemotePackage) {
|
function getRequiredPackages(pkg: RemotePackage) {
|
||||||
const ret: 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);
|
const found = packages?.find((p) => p.package_id === r.package_id);
|
||||||
if (found && !ret.includes(found)) {
|
if (found && !ret.includes(found)) {
|
||||||
ret.push(found);
|
ret.push(found);
|
||||||
ret.push(...getRequiredPackages(found));
|
ret.push(...getRequiredPackages(found));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -698,10 +761,7 @@ const SourcePackagesList: React.FC<{
|
||||||
return prev.filter((n) => n.package_id !== pkg.package_id);
|
return prev.filter((n) => n.package_id !== pkg.package_id);
|
||||||
} else {
|
} else {
|
||||||
// also include required packages
|
// also include required packages
|
||||||
const toAdd = [pkg];
|
return [...prev, pkg, ...getRequiredPackages(pkg)];
|
||||||
toAdd.push(...getRequiredPackages(pkg));
|
|
||||||
|
|
||||||
return prev.concat(...toAdd);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -711,27 +771,19 @@ const SourcePackagesList: React.FC<{
|
||||||
key={pkg.package_id}
|
key={pkg.package_id}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
pkg={pkg}
|
pkg={pkg}
|
||||||
requiredBy={selectedPackages.filter((p) => {
|
requiredBy={selectedPackages.filter((p) =>
|
||||||
return p.requires.find((r) => r.package_id === pkg.package_id);
|
p.requires.some((r) => r.package_id === pkg.package_id)
|
||||||
})}
|
)}
|
||||||
selected={checkedMap[pkg.package_id] ?? false}
|
selected={checkedMap[pkg.package_id] ?? false}
|
||||||
togglePackage={() => togglePackage(pkg)}
|
togglePackage={() => togglePackage(pkg)}
|
||||||
renderDescription={renderDescription}
|
renderDescription={renderDescription}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [
|
}
|
||||||
filteredPackages,
|
|
||||||
disabled,
|
|
||||||
checkedMap,
|
|
||||||
selectedPackages,
|
|
||||||
setSelectedPackages,
|
|
||||||
packages,
|
|
||||||
renderDescription,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr key={source.url} className="package-source">
|
<tr className="package-source">
|
||||||
<td>
|
<td>
|
||||||
{packages !== undefined ? (
|
{packages !== undefined ? (
|
||||||
<Form.Check
|
<Form.Check
|
||||||
|
|
@ -741,8 +793,20 @@ const SourcePackagesList: React.FC<{
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</td>
|
</td>
|
||||||
<td>{renderCollapseButton()}</td>
|
<td className="source-collapse">
|
||||||
<td colSpan={2} onClick={() => toggleSourceOpen()}>
|
<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>
|
<span>{source.name ?? source.url}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="source-controls">
|
<td className="source-controls">
|
||||||
|
|
@ -764,32 +828,7 @@ const SourcePackagesList: React.FC<{
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{loading ? (
|
{renderContents()}
|
||||||
<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}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AlertModal
|
<AlertModal
|
||||||
|
|
@ -880,8 +971,8 @@ const AvailablePackagesList: React.FC<{
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="button-cell"></th>
|
<th className="check-cell"></th>
|
||||||
<th className="button-cell"></th>
|
<th className="collapse-cell"></th>
|
||||||
<th>
|
<th>
|
||||||
<FormattedMessage id="package_manager.package" />
|
<FormattedMessage id="package_manager.package" />
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -893,49 +984,7 @@ const AvailablePackagesList: React.FC<{
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>{renderBody()}</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>
|
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,19 @@
|
||||||
.package-source {
|
.package-source {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
|
.source-collapse {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.source-controls {
|
.source-controls {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,10 +26,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-collapse-button {
|
|
||||||
color: $text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.package-manager-table-container {
|
.package-manager-table-container {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -31,39 +37,54 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
.button-cell {
|
.check-cell {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collapse-cell {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table td {
|
table td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.package-name,
|
||||||
|
.package-id,
|
||||||
.package-version,
|
.package-version,
|
||||||
.package-date,
|
.package-date,
|
||||||
.package-name,
|
.package-latest-version,
|
||||||
.package-id {
|
.package-latest-date {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.package-id,
|
||||||
.package-date,
|
.package-date,
|
||||||
.package-id {
|
.package-latest-date {
|
||||||
color: $muted-gray;
|
color: $muted-gray;
|
||||||
font-size: 0.8rem;
|
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 {
|
.package-manager-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
gap: 0.5rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-required-by {
|
.package-required-by {
|
||||||
|
|
|
||||||
|
|
@ -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 { Modifiers } from "@apollo/client/cache";
|
||||||
import {
|
import {
|
||||||
isField,
|
isField,
|
||||||
|
|
@ -1961,43 +1966,6 @@ export const mutateSubmitStashBoxPerformerDraft = (
|
||||||
variables: { input },
|
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
|
/// Configuration
|
||||||
|
|
||||||
export const useConfiguration = () => GQL.useConfigurationQuery();
|
export const useConfiguration = () => GQL.useConfigurationQuery();
|
||||||
|
|
@ -2043,6 +2011,65 @@ export const useJobsSubscribe = () => GQL.useJobsSubscribeSubscription();
|
||||||
|
|
||||||
export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
|
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) {
|
function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
|
||||||
if (!result.data) return;
|
if (!result.data) return;
|
||||||
|
|
||||||
|
|
@ -2051,7 +2078,15 @@ function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
|
||||||
|
|
||||||
export const useConfigureGeneral = () =>
|
export const useConfigureGeneral = () =>
|
||||||
GQL.useConfigureGeneralMutation({
|
GQL.useConfigureGeneralMutation({
|
||||||
update: updateConfiguration,
|
update(cache, result) {
|
||||||
|
if (!result.data?.configureGeneral) return;
|
||||||
|
|
||||||
|
evictQueries(cache, [
|
||||||
|
GQL.ConfigurationDocument,
|
||||||
|
...scraperMutationImpactedQueries,
|
||||||
|
...pluginMutationImpactedQueries,
|
||||||
|
]);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useConfigureInterface = () =>
|
export const useConfigureInterface = () =>
|
||||||
|
|
@ -2097,48 +2132,6 @@ export const useAddTempDLNAIP = () => GQL.useAddTempDlnaipMutation();
|
||||||
|
|
||||||
export const useRemoveTempDLNAIP = () => GQL.useRemoveTempDlnaipMutation();
|
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) =>
|
export const mutateStopJob = (jobID: string) =>
|
||||||
client.mutate<GQL.StopJobMutation>({
|
client.mutate<GQL.StopJobMutation>({
|
||||||
mutation: GQL.StopJobDocument,
|
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
|
/// Tasks
|
||||||
|
|
||||||
export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>
|
export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>
|
||||||
|
|
|
||||||
|
|
@ -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_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",
|
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
|
||||||
"parallel_scan_head": "Parallel Scan/Generation",
|
"parallel_scan_head": "Parallel Scan/Generation",
|
||||||
|
"plugins_path": {
|
||||||
|
"description": "Directory location of plugin configuration files",
|
||||||
|
"heading": "Plugins Path"
|
||||||
|
},
|
||||||
"preview_generation": "Preview Generation",
|
"preview_generation": "Preview Generation",
|
||||||
"python_path": {
|
"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",
|
"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",
|
"selected_only": "Selected only",
|
||||||
"show_all": "Show all",
|
"show_all": "Show all",
|
||||||
"uninstall": "Uninstall",
|
"uninstall": "Uninstall",
|
||||||
|
"unknown": "<unknown>",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue