Skip to content

Commit

Permalink
refactor: Simplify the mod page filter and search
Browse files Browse the repository at this point in the history
  • Loading branch information
ci010 committed May 5, 2023
1 parent 8be4f0f commit c13792f
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 229 deletions.
171 changes: 79 additions & 92 deletions xmcl-keystone-ui/src/composables/mod.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useRefreshable, useService } from '@/composables'
import { isStringArrayEquals } from '@/util/equal'
import { getModDependencies, getModProvides, ModDependencies } from '@/util/modDependencies'
import { InstanceJavaServiceKey, InstanceModsServiceKey, InstanceServiceKey, isModResource, isPersistedResource, JavaRecord, Resource, ResourceServiceKey, RuntimeVersions } from '@xmcl/runtime-api'
import { computed, InjectionKey, ref, Ref, watch } from 'vue'
import { useService } from '@/composables'
import { AggregateExecutor } from '@/util/aggregator'
import { ModDependencies, getModDependencies, getModProvides } from '@/util/modDependencies'
import { InstanceModsServiceKey, JavaRecord, Resource, ResourceServiceKey, RuntimeVersions, isPersistedResource } from '@xmcl/runtime-api'
import { InjectionKey, Ref, computed, ref, watch } from 'vue'

export const kModsContext: InjectionKey<{
/**
Expand Down Expand Up @@ -35,19 +35,22 @@ export interface ModItem {
* Mod version
*/
version: string
/**
* The mod description text
*/
description: string
/**
* Mod icon url
*/
icon: string

/**
* The mod loaders
*/
modLoaders: string[]

/**
* The resource tag
*/
tags: string[]

/**
* The hash of the resource
*/
Expand All @@ -56,25 +59,33 @@ export interface ModItem {
* The universal location of the mod
*/
url: string

/**
* All mod dependencies
*/
dependencies: ModDependencies
/**
* The provided runtime
*/
provideRuntime: Record<string, string>

/**
* The pending enabled. Might be different from the actual enable state
* If this mod is enabled. This is computed from the path suffix.
*/
enabled: boolean
/**
* The actual enabled state. Represent if the mod is moved to the mods folder.
* The backed resource
*/
enabledState: boolean
resource: Resource

subsequence: boolean
// State props

/**
* Is this mod is selected
*/
selected: boolean
/**
* Is this mod is dragged
*/
dragged: boolean

resource: Resource
}

/**
Expand All @@ -86,40 +97,13 @@ export function useInstanceMods(runtimes: Ref<RuntimeVersions>, java: Ref<JavaRe
const { showDirectory } = useService(InstanceModsServiceKey)

const items: Ref<ModItem[]> = ref([])
const pendingUninstallItems = computed(() => items.value.filter(i => !i.enabled && i.enabledState))
const pendingInstallItems = computed(() => items.value.filter(i => i.enabled && !i.enabledState))
const pendingEditItems = computed(() => items.value.filter(i => (isPersistedResource(i.resource) && !isStringArrayEquals(i.tags, i.resource.tags))))
const isModified = computed(() => pendingInstallItems.value.length > 0 || pendingUninstallItems.value.length > 0 || pendingEditItems.value.length > 0)

const { refresh: commit, refreshing: committing } = useRefreshable(async () => {
const promises: Promise<any>[] = []

promises.push(updateResources(pendingEditItems.value.map(i => ({
...i.resource,
name: i.name,
tags: i.tags,
}))))
if (pendingInstallItems.value.length > 0) {
promises.push(enable({ mods: pendingInstallItems.value.map(v => v.resource) }))
}
if (pendingUninstallItems.value.length > 0) {
promises.push(disable({ mods: pendingUninstallItems.value.map(v => v.resource) }))
}

await Promise.all(promises)
})

const cachedItems = new Map<string, ModItem>()
const iconMap: Ref<Record<string, string>> = ref({})
const enabledModCounts = ref(0)

function updateItems() {
const newItems = state.mods.map(getModItemFromResource)
for (const item of newItems) {
item.enabled = !item.path.endsWith('.disabled')
item.enabledState = item.enabled
}
const enabledModCounts = computed(() => items.value.filter(v => v.enabled).length)

function updateItems(resources: Resource[]) {
const newItems = resources.map(getModItemFromResource)
const newIconMap: Record<string, string> = {}

for (const item of newItems) {
Expand All @@ -139,9 +123,8 @@ export function useInstanceMods(runtimes: Ref<RuntimeVersions>, java: Ref<JavaRe
cachedItems.set(item.hash, item)
}

iconMap.value = newIconMap
iconMap.value = markRaw(newIconMap)
items.value = newItems
enabledModCounts.value = newItems.filter(v => !v.path.endsWith('.disabled')).length
}

const currentRuntime = computed(() => {
Expand All @@ -151,7 +134,7 @@ export function useInstanceMods(runtimes: Ref<RuntimeVersions>, java: Ref<JavaRe
}
runtime.fabricloader = runtime.fabricLoader
for (const i of items.value) {
if (i.enabled || i.enabledState) {
if (i.enabled) {
for (const [key, val] of Object.entries(i.provideRuntime)) {
runtime[key] = val
}
Expand All @@ -166,36 +149,29 @@ export function useInstanceMods(runtimes: Ref<RuntimeVersions>, java: Ref<JavaRe
icons: iconMap,
})

watch(computed(() => state.mods), (val) => {
updateItems()
})

function getUrl(resource: Resource) {
return resource.uris.find(u => u?.startsWith('http')) ?? ''
}
function getModItemFromModResource(resource: Resource): ModItem {

function getModItemFromResource(resource: Resource): ModItem {
const isPersisted = isPersistedResource(resource)
const dependencies = markRaw(getModDependencies(resource))
const provideRuntime = markRaw(getModProvides(resource))
const modItem: ModItem = ({
path: resource.path,
id: '',
name: resource.path,
version: '',
modLoaders: markRaw([]),
description: '',
provideRuntime,
provideRuntime: markRaw(getModProvides(resource)),
icon: resource.icons?.at(-1) ?? '',
dependencies,
dependencies: markRaw(getModDependencies(resource)),
url: getUrl(resource),
hash: resource.hash,
tags: isPersisted ? [...resource.tags] : [],
enabled: false,
enabledState: false,
subsequence: false,
enabled: !resource.path.endsWith('.disabled'),
selected: false,
dragged: false,
resource,
resource: markRaw(resource),
})
if (resource.metadata.forge) {
modItem.modLoaders.push('forge')
Expand Down Expand Up @@ -248,44 +224,55 @@ export function useInstanceMods(runtimes: Ref<RuntimeVersions>, java: Ref<JavaRe
return reactive(modItem)
}

function getModItemFromResource(resource: Resource): ModItem {
if (isModResource(resource)) {
return getModItemFromModResource(resource)
}
const isPersisted = isPersistedResource(resource)
return reactive({
path: resource.path,
id: resource.hash,
name: resource.fileName,
provideRuntime: {},
modLoaders: [],
dependencies: [],
version: '',
description: '',
icon: resource.icons?.[0] || '',
url: getUrl(resource),
hash: resource.hash,
tags: isPersisted ? [...resource.tags] : [],
enabled: false,
enabledState: false,
subsequence: false,
hide: false,
selected: false,
dragged: false,
resource: markRaw(resource),
})
const updating = ref(false)
const executor = new AggregateExecutor<[ModItem, 'enable' | 'disable' | 'update'], [ModItem, 'enable' | 'disable' | 'update'][]>(
(v) => v, (cmd) => {
const toEnable = cmd.filter(c => c[1] === 'enable')
const toDisable = cmd.filter(c => c[1] === 'disable')
const toUpdate = cmd.filter(c => c[1] === 'update')
Promise.all([
enable({ mods: toEnable.map(e => e[0].resource) }),
disable({ mods: toDisable.map(e => e[0].resource) }),
updateResources(toUpdate.map(([item]) => ({
...item.resource,
name: item.name,
tags: item.tags,
}))),
]).finally(() => {
updating.value = false
})
}, 800)

function enableMod(item: ModItem) {
updating.value = true
executor.push([item, 'enable'])
}

function disableMod(item: ModItem) {
updating.value = true
executor.push([item, 'disable'])
}

function updateTag(item: ModItem) {
updating.value = true
executor.push([item, 'update'])
}

onMounted(() => {
updateItems()
updateItems(state.mods)
})

watch(computed(() => state.mods), (val) => {
updateItems(val)
})

return {
isModified,
items,
updating,
enableMod,
disableMod,
updateTag,
enabledModCounts,
commit,
committing,
showDirectory,
}
}
54 changes: 1 addition & 53 deletions xmcl-keystone-ui/src/composables/modFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,64 +11,12 @@ export function useModFilter(items: Ref<ModItem[]>) {
...item.tags.map(t => ({ type: 'tag', value: t, label: 'label' })),
]
}
const modLoaderFilters = ref(['forge', 'fabric', 'quilt', 'optifine'] as string[])
const filterOptions = computed(() => items.value.map(getFilterOptions).reduce((a, b) => [...a, ...b], []))
const { filter } = useFilterCombobox<ModItem>(filterOptions, getFilterOptions, (v) => `${v.name} ${v.version}`)
const modLoaderRecord = ref(new Set(modLoaderFilters.value))

const updateModLoaderSet = debounce(() => {
modLoaderRecord.value = new Set(modLoaderFilters.value)
}, 700)

watch(modLoaderFilters, updateModLoaderSet)

function isCompatibleMod(mod: ModItem) {
if (mod.enabled) {
return true
}
for (const loader of mod.modLoaders) {
// is modloader is disabled?
if (!modLoaderRecord.value.has(loader)) {
return false
}
}
return true
}

const filtered = computed(() => filter(items.value))
const filteredCompatbile = computed(() => filtered.value.filter(isCompatibleMod))
const mods = computed(() => {
const enabled: Record<string, ModItem[]> = {}
const disabled: Record<string, ModItem[]> = {}
for (const mod of filteredCompatbile.value) {
if (mod.enabledState) {
if (enabled[mod.name]) {
mod.subsequence = true
enabled[mod.name].push(mod)
} else {
enabled[mod.name] = [mod]
}
} else {
if (disabled[mod.name]) {
mod.subsequence = true
disabled[mod.name].push(mod)
} else {
disabled[mod.name] = [mod]
}
}
}
const grouped: ModItem[] = []
for (const mod of Object.values(enabled)) {
grouped.push(...mod)
}
for (const mod of Object.values(disabled)) {
grouped.push(...mod)
}
return grouped
})

return {
items: mods,
modLoaderFilters,
items: filtered,
}
}
2 changes: 2 additions & 0 deletions xmcl-keystone-ui/src/composables/modSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export function useModsSearch(keyword: Ref<string>, runtime: Ref<InstanceData['r
const isValidResource = (r: Resource) => {
const useForge = !!runtime.value.forge
const useFabric = !!runtime.value.fabricLoader
const useQuilt = !!runtime.value.quiltLoader
if (useForge) return !!r.metadata.forge
if (useFabric) return !!r.metadata.fabric
if (useQuilt) return !!r.metadata.quilt
return false
}
const mods = computed(() => keyword.value
Expand Down
16 changes: 14 additions & 2 deletions xmcl-keystone-ui/src/composables/modSelection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Ref } from 'vue'
import { ModItem } from './mod'

export function useModSelection(items: Ref<ModItem[]>) {
export function useModSelection(items: Ref<ModItem[]>, enable: (item: ModItem) => void, disable: (item: ModItem) => void) {
const isSelectionMode = ref(false)
const selectedItems = computed(() => items.value.filter(i => i.selected))

Expand Down Expand Up @@ -56,9 +56,21 @@ export function useModSelection(items: Ref<ModItem[]>) {
}
function onEnable({ item, enabled }: { item: ModItem; enabled: boolean }) {
if (item.selected) {
selectedItems.value.forEach(i => { i.enabled = enabled })
selectedItems.value.forEach(i => {
i.enabled = enabled
if (enabled) {
enable(i)
} else {
disable(i)
}
})
} else {
item.enabled = enabled
if (enabled) {
enable(item)
} else {
disable(item)
}
}
}
onMounted(() => {
Expand Down

0 comments on commit c13792f

Please sign in to comment.