Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mod dependencies and by extension mod packs #60

Merged
merged 6 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type EntryIndexAuthorsItem = {
url?: string
}

export interface EntryIndex {
interface EntryIndexBase {
authors: EntryIndexAuthorsItem[]
/** The category of the mod, this is used to group mods in the mod browser */
category?: string
Expand All @@ -63,10 +63,25 @@ export interface EntryIndex {
name?: string
/** The tags of the mod, these are used to filter mods in the mod browser */
tags: string[]

/** The versions of the mod */
versions: EntryIndexVersionsItem[]
}

export interface EntryIndex extends EntryIndexBase {
/** Mod Ids this mod is dependent on */
dependencies?: string[]
}

export interface EntryIndexSimple {
id: string
name: string
}

export interface EntryIndexHydrated extends EntryIndexBase {
dependencies?: EntryIndexSimple[]
}

export type RegistryIndexItemAuthorsItem = {
avatar?: string
name: string
Expand Down
7 changes: 5 additions & 2 deletions src/main/manager/lifecycle-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,18 @@ export class LifecycleManager {
* If the mod is enabled, it will be disabled
* If the mod is disabled, it will be enabled
*/
async toggleMod(modId: string): Promise<void> {
async toggleMod(modId: string, enableOnly: boolean | undefined): Promise<boolean> {
john681611 marked this conversation as resolved.
Show resolved Hide resolved
const { release } = await this.getSubscriptionWithReleaseOrThrow(modId)
this.logger.debug(`Toggling mod: ${modId} which is currently ${release.enabled}`)
if (release.enabled) {
if (release.enabled && enableOnly !== true) {
this.logger.debug(`Disabling mod: ${modId}`)
await this.disableMod(modId)
return false
} else {
if (enableOnly === true && release.enabled) return true
this.logger.debug(`Enabling mod: ${modId}`)
await this.enableMod(modId)
return true
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/manager/subscription.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ export class SubscriptionManager implements OnApplicationBootstrap {
id: randomUUID(),
modId: mod.id,
modName: mod.name!,
created: Date.now()
created: Date.now(),
dependencies: mod.dependencies || []
}
const release: Release = {
id: randomUUID(),
Expand Down
5 changes: 3 additions & 2 deletions src/main/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export function getAppRouter(moduleRef: ModuleRef) {
}),
// Enable/Disable Toggle
toggleMod: trpc.procedure
.input(z.object({ modId: z.string() }))
.input(z.object({ modId: z.string(), enableOnly: z.boolean().optional() }))
.mutation(
async ({ input }): Promise<void> => moduleRef.get(LifecycleManager).toggleMod(input.modId)
async ({ input }): Promise<boolean> =>
moduleRef.get(LifecycleManager).toggleMod(input.modId, input.enableOnly)
),
getModAssets: trpc.procedure
.input(z.object({ modId: z.string() }))
Expand Down
4 changes: 4 additions & 0 deletions src/main/schemas/subscription.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { EntryIndexSimple } from '../../lib/client'

@Schema({ timestamps: true })
export class Subscription {
Expand All @@ -8,6 +9,9 @@ export class Subscription {
@Prop({ required: true, index: true })
modId: string

@Prop({ required: false, index: true })
dependencies: EntryIndexSimple[]

@Prop({ required: true })
modName: string

Expand Down
24 changes: 20 additions & 4 deletions src/main/services/registry.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'
import { SettingsManager } from '../manager/settings.manager'
import {
EntryIndex,
EntryIndexHydrated,
EntryIndexSimple,
EntryIndexVersionsItem,
getRegistryEntry,
getRegistryIndex,
Expand All @@ -20,11 +21,26 @@ export class RegistryService {
return data
}

async getRegistryEntryIndex(modId: string): Promise<EntryIndex> {
async getRegistryEntryIndex(modId: string): Promise<EntryIndexHydrated> {
const baseURL = await this.settingsManager.getRegistryUrl()
const { data } = await getRegistryEntry(modId, {
baseURL: await this.settingsManager.getRegistryUrl()
baseURL
})
return data
const hydratedData = data as EntryIndexHydrated
if (data.dependencies) {
hydratedData.dependencies = await Promise.all(
data.dependencies.map<Promise<EntryIndexSimple>>(async (it) => {
const { data: depData } = await getRegistryEntry(it, {
baseURL
})
return {
id: depData.id,
name: depData.name
} as EntryIndexSimple
})
)
}
return hydratedData
}

async getLatestVersion(modId: string): Promise<EntryIndexVersionsItem | undefined> {
Expand Down
3 changes: 2 additions & 1 deletion src/main/services/subscription.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ describe('SubscriptionService', () => {
id: 'test-subscription',
modId: 'test-mod',
modName: 'Test Subscription',
created: Date.now()
created: Date.now(),
dependencies: []
}

beforeAll(async () => {
Expand Down
34 changes: 34 additions & 0 deletions src/renderer/src/components/subscription-row.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ActionIcon, Group, Menu, Progress, Stack, Table, Text, Tooltip } from '@mantine/core'
import { BiCheckbox, BiCheckboxChecked, BiPlay } from 'react-icons/bi'
import { BsThreeDotsVertical } from 'react-icons/bs'
import { EntryIndexSimple } from 'src/lib/client'

function SubscriptionStatusColumn(props: {
isLatest: boolean
Expand All @@ -10,6 +11,7 @@ function SubscriptionStatusColumn(props: {
latestVersion?: string
progress: number
errors: string[]
missingDeps: EntryIndexSimple[]
}) {
if (props.isReady && props.errors.length > 0) {
const errors = props.errors.map((error, index) => (
Expand All @@ -29,6 +31,30 @@ function SubscriptionStatusColumn(props: {
)
}

if (props.isReady && props.missingDeps.length > 0) {
return (
<Table.Td>
<Tooltip
multiline
withinPortal={false}
label={
<Stack gap={'xs'}>
<Text size={'sm'}>Missing Dependencies:</Text>
{props.missingDeps.map((dependency) => (
<Text key={dependency.id}>- {dependency.name || dependency.id}</Text>
))}
</Stack>
}
>
<Text fw={'bold'} c={'red'} size={'sm'}>
{props.missingDeps.length} Missing{' '}
{props.missingDeps.length > 1 ? 'Dependencies' : 'Dependency'}
</Text>
</Tooltip>
</Table.Td>
)
}

if (props.isReady && props.isLatest) {
return <Table.Td>Up to Date</Table.Td>
}
Expand Down Expand Up @@ -73,13 +99,15 @@ export type SubscriptionRowProps = {
onOpenInExplorer: () => void
onToggleMod: () => void
onUpdate: () => void
onFixMissingDeps: () => void
onRunExe?: () => void
onUnsubscribe: () => void
isReady: boolean
isFailed: boolean
stateLabel: string
progress: number
errors: string[]
missingDeps: EntryIndexSimple[]
}

export function SubscriptionRow(props: SubscriptionRowProps) {
Expand Down Expand Up @@ -129,6 +157,7 @@ export function SubscriptionRow(props: SubscriptionRowProps) {
progress={props.progress}
isLatest={props.isLatest}
latestVersion={props.latestVersion}
missingDeps={props.missingDeps}
/>
<Table.Td>{new Date(props.created).toLocaleString()}</Table.Td>
<Table.Td>
Expand All @@ -143,6 +172,11 @@ export function SubscriptionRow(props: SubscriptionRowProps) {
{props.isReady && props.errors && props.isLatest && (
<Menu.Item onClick={props.onUpdate}>Resubscribe</Menu.Item>
)}
{props.isReady && props.missingDeps.length > 0 && (
<Menu.Item onClick={props.onFixMissingDeps}>
Subscribe to missing Dependencies
</Menu.Item>
)}
<Menu.Item
onClick={() => props.onToggleMod()}
disabled={!props.isReady || props.errors.length > 0}
Expand Down
39 changes: 36 additions & 3 deletions src/renderer/src/container/subscriptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { SubscriptionRow } from '../components/subscription-row'
import { useNavigate } from 'react-router-dom'
import { useEffect } from 'react'
import { showNotification } from '@mantine/notifications'
import { EntryIndexSimple } from 'src/lib/client'
import { Subscription } from 'src/main/schemas/subscription.schema'
import { SubscriptionWithState } from 'src/main/manager/subscription.manager'

export type SubscriptionsProps = {
onOpenSymlinksModal: (modId: string) => void
Expand Down Expand Up @@ -58,9 +61,16 @@ export function Subscriptions({ onOpenSymlinksModal }: SubscriptionsProps) {
}
}

async function handleToggleMod(modId: string) {
async function handleToggleMod(sub: Subscription) {
try {
await client.toggleMod.mutate({ modId })
const isEnabled = await client.toggleMod.mutate({ modId: sub.modId })
if (sub.dependencies && isEnabled) {
await Promise.all(
sub.dependencies.map((dep) =>
client.toggleMod.mutate({ modId: dep.id, enableOnly: true })
)
)
}
await subscriptions.mutate()
} catch (err) {
showErrorNotification(err)
Expand All @@ -80,6 +90,23 @@ export function Subscriptions({ onOpenSymlinksModal }: SubscriptionsProps) {
await subscriptions.mutate()
}

async function handleSubscribeToMissingDeps(
dependencies: EntryIndexSimple[],
subscriptions: SubscriptionWithState[]
) {
for (const dependency of dependencies) {
if (subscriptions.some((sub) => sub.subscription.modId === dependency.id)) continue
try {
await client.subscribe.mutate({ modId: dependency.id })
showSuccessNotification(`Subscribed to ${dependency.name}`)
} catch (e) {
// Ignore errors for now
console.error('Failed to subscribe to dependency:', dependency, e)
showErrorNotification(e)
}
}
}

return (
<Stack>
<TextInput
Expand Down Expand Up @@ -128,6 +155,9 @@ export function Subscriptions({ onOpenSymlinksModal }: SubscriptionsProps) {
created={subscription.created}
onOpenSymlinksModal={() => onOpenSymlinksModal(subscription.modId)}
onUpdate={() => handleUpdate(subscription.modId)}
onFixMissingDeps={() =>
handleSubscribeToMissingDeps(subscription.dependencies, results)
}
onUnsubscribe={() => handleUnsubscribe(subscription.modId)}
onViewModPage={() => navigate(`/library/${subscription.modId}`)}
progress={state.progress}
Expand All @@ -136,8 +166,11 @@ export function Subscriptions({ onOpenSymlinksModal }: SubscriptionsProps) {
isLatest={state.isLatest}
version={state.version}
enabled={state.enabled}
onToggleMod={() => handleToggleMod(subscription.modId)}
onToggleMod={() => handleToggleMod(subscription)}
stateLabel={state.currentTaskLabel || state.progressLabel}
missingDeps={subscription.dependencies?.filter(
(dep) => !results.some((sub) => sub.subscription.modId === dep.id)
)}
onRunExe={
state.exePath
? () => state.exePath && handleRunExe(subscription.modId, state.exePath)
Expand Down
16 changes: 14 additions & 2 deletions src/renderer/src/hooks/useRegistrySubscriber.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { EntryIndex } from '../../../lib/client'
import { EntryIndexHydrated } from '../../../lib/client'
import { client } from '../client'
import { showErrorNotification, showSuccessNotification } from '../utils/notifications'
import { useSubscriptions } from './useSubscriptions'

export const useRegistrySubscriber = (registryEntry: EntryIndex) => {
export const useRegistrySubscriber = (registryEntry: EntryIndexHydrated) => {
const allSubscriptions = useSubscriptions()

return {
Expand All @@ -12,6 +12,18 @@ export const useRegistrySubscriber = (registryEntry: EntryIndex) => {
try {
console.log('Subscribed to registry entry:', registryEntry)
await client.subscribe.mutate({ modId: registryEntry.id })
if (registryEntry.dependencies) {
for (const dependency of registryEntry.dependencies) {
try {
await client.subscribe.mutate({ modId: dependency.id })
showSuccessNotification(`Subscribed to ${dependency.name}`)
} catch (e) {
// Ignore errors for now
console.error('Failed to subscribe to dependency:', dependency, e)
showErrorNotification(e)
}
}
}
showSuccessNotification(`Subscribed to ${registryEntry.name}`)
} catch (e) {
showErrorNotification(e)
Expand Down
27 changes: 25 additions & 2 deletions src/renderer/src/pages/registry-entry.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import React, { useEffect } from 'react'
import { MdOutlineCategory } from 'react-icons/md'
import { VscCheck, VscClose } from 'react-icons/vsc'
import { useNavigate, useParams } from 'react-router-dom'
import { EntryIndex } from '../../../lib/client'
import { EntryIndexHydrated } from '../../../lib/client'
import { ReleaseSummary } from '../components/release-summary'
import { useRegistrySubscriber } from '../hooks/useRegistrySubscriber'
import { useRegistryEntry } from '../hooks/useRegistryEntry'
Expand Down Expand Up @@ -62,7 +62,7 @@ export const RegistryEntryPage: React.FC = () => {
}

export type RegistryEntryPageProps = {
entry: EntryIndex
entry: EntryIndexHydrated
}
export const _RegistryEntryPage: React.FC<RegistryEntryPageProps> = ({ entry }) => {
const navigate = useNavigate()
Expand Down Expand Up @@ -170,6 +170,29 @@ export const _RegistryEntryPage: React.FC<RegistryEntryPageProps> = ({ entry })

<Divider color={'gray'} />

<Divider color={'gray'} />
<Stack gap={'xs'}>
<Group justify={'space-between'}>
<Title order={4} fw={500}>
Dependencies
</Title>
</Group>
{entry.dependencies ? (
<Stack gap={'xs'}>
{entry.dependencies.map((dependency) => (
<Anchor
key={dependency.id}
onClick={() => navigate(`/library/${dependency.id}`)}
>
{dependency.name || dependency.id}
</Anchor>
))}
</Stack>
) : (
<Text>No Dependencies Found</Text>
)}
</Stack>

<Stack gap={'xs'}>
<Group justify={'space-between'}>
<Title order={4} fw={500}>
Expand Down