Skip to content

Commit

Permalink
Add mod star support
Browse files Browse the repository at this point in the history
  • Loading branch information
krypciak committed Feb 8, 2024
1 parent bdd1f70 commit aa99222
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 66 deletions.
11 changes: 11 additions & 0 deletions assets/data/lang/sc/gui.en_US.json.patch
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
"menu": {
"menu-titles": {
"mods": "Mods"
},
"sort": {
"stars": "Stars",
"nameReverse": "Name (desencing)",
"dec": {
"stars": "Sorts by GitHub stars.",
"nameReverse": "Sorts by name in reverse."
}
},
"ccmodloader": {
"areYouSure": "Are you sure you want to install: \\c[3][modName]\\c[0]?"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 52 additions & 6 deletions src/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ModEntry, ModImageConfig as ModIconConfig, NPDatabase } from './types'

const fs: typeof import('fs') = (0, eval)("require('fs')")
import type { IncomingMessage } from 'http'
const http: typeof import('http') = (0, eval)("require('http')")
const https: typeof import('https') = (0, eval)("require('https')")

async function* getFilesRecursive(dir: string): AsyncIterable<string> {
const dirents = await fs.promises.readdir(dir, { withFileTypes: true })
Expand All @@ -14,6 +17,29 @@ async function* getFilesRecursive(dir: string): AsyncIterable<string> {
}
}

function getTag(head: IncomingMessage): string {
switch (typeof head.headers.etag) {
case 'string':
return head.headers.etag
case 'object':
return head.headers.etag[0]
default:
return ''
}
}

async function getETag(url: string): Promise<string> {
const uri = new URL(url)
const { get } = uri.protocol === 'https:' ? https : http

const head: IncomingMessage = await new Promise((resolve, reject) =>
get(url, { method: 'HEAD' })
.on('response', resp => resolve(resp))
.on('error', err => reject(err))
)
return getTag(head)
}

export class FileCache {
private static cacheDir: string

Expand Down Expand Up @@ -65,18 +91,38 @@ export class FileCache {
return ccPath
}

static async getDatabase(name: string): Promise<NPDatabase> {
const path = `${name}.json`
const cached = await this.getCachedFile<NPDatabase>(path, true)
if (cached) return cached

const url = `${this.databases[name]}/npDatabase.json`
private static async downloadAndWriteDatabase(path: string, url: string, eTag: string) {
const data: NPDatabase = (this.cache[path] = await (await fetch(url)).json())
data.eTag = eTag
fs.writeFile(`${this.cacheDir}/${path}`, JSON.stringify(data), () => {})
this.inCache.add(path)
return data
}

static async getDatabase(name: string, create: (database: NPDatabase) => void): Promise<void> {
const path = `${name}.json`
const url = `${this.databases[name]}/npDatabase.json`

const cachedPromise = this.getCachedFile<NPDatabase>(path, true)
let eTag!: string
const eTagPromise = getETag(url).then(async newEtag => {
eTag = newEtag
const cached = await cachedPromise
if (cached && cached.eTag != eTag) {
const data = await this.downloadAndWriteDatabase(path, url, eTag)
create(data)
}
})

const cached = await cachedPromise
if (cached) return create(cached)

await eTagPromise
if (!eTag) throw new Error('eTag unset somehow')
const data = await this.downloadAndWriteDatabase(path, url, eTag)
create(data)
}

private static async getCachedFile<T>(path: string, toJSON: boolean = false): Promise<T | undefined> {
if (!this.inCache.has(path)) return
const cached = this.cache[path]
Expand Down
73 changes: 56 additions & 17 deletions src/gui/list-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { ModListEntryHighlight } from './list-entry-highlight'
import { ModMenuList } from './list'
import { ModEntry } from '../types'
import { FileCache } from '../cache'
import { databases } from '../moddb'

export interface ModListEntry extends ig.FocusGui {
ninepatch: ig.NinePatch
mod: ModEntry
nameText: sc.TextGui
description?: sc.TextGui
description: sc.TextGui
versionText: sc.TextGui
starCount?: sc.TextGui
installRemoveButton: sc.ButtonGui
checkForUpdatesButton: sc.ButtonGui
openModSettingsButton: sc.ButtonGui
Expand All @@ -16,6 +19,8 @@ export interface ModListEntry extends ig.FocusGui {
modEntryActionButtonStart: { height: number; ninepatch: ig.NinePatch; highlight: sc.ButtonGui.Highlight }
modEntryActionButtons: sc.ButtonGui.Type & { ninepatch: ig.NinePatch }
iconGui: ig.ImageGui

askInstall(this: this): void
}
interface ModListEntryConstructor extends ImpactClass<ModListEntry> {
new (mod: ModEntry, modList: ModMenuList): ModListEntry
Expand Down Expand Up @@ -76,38 +81,48 @@ export const ModListEntry: ModListEntryConstructor = ig.FocusGui.extend({

init(mod, modList) {
this.parent()
this.mod = mod
this.modList = modList

/* init icon asap */
FileCache.getIconConfig(mod).then(config => {
const image = new ig.Image(config.path)
this.iconGui = new ig.ImageGui(image, config.offsetX, config.offsetY, config.sizeX, config.sizeY)
this.iconGui.setPos(2, 8)
this.addChildGui(this.iconGui)
})

const buttonSquareSize = 14

this.setSize(modList.hook.size.x - 3 /* 3 for scrollbar */, 43 /*modList.entrySize*/)
this.setSize(modList.hook.size.x - 3 /* 3 for scrollbar */, buttonSquareSize * 3 - 3)

this.nameText = new sc.TextGui(mod.name)

const iconOffset = 25 as const
this.highlight = new ModListEntryHighlight(this.hook.size.x, this.hook.size.y, this.nameText.hook.size.x, buttonSquareSize * 3)
this.highlight.setPos(iconOffset, 0)
this.addChildGui(this.highlight)
this.addChildGui(this.nameText)

this.description = new sc.TextGui(mod.description ?? '', { font: sc.fontsystem.smallFont })
this.addChildGui(this.description)

const iconOffset = 24 + 1
this.nameText.setPos(4 + iconOffset, 0)
this.description.setPos(4 + iconOffset, 14)

this.versionText = new sc.TextGui(mod.version, { font: sc.fontsystem.tinyFont })

this.versionText = new sc.TextGui(`v${mod.version}`, { font: sc.fontsystem.tinyFont })
this.versionText.setAlign(ig.GUI_ALIGN.X_RIGHT, ig.GUI_ALIGN.Y_TOP)
this.versionText.setPos(3, 3)
this.addChildGui(this.versionText)

FileCache.getIconConfig(mod).then(config => {
const image = new ig.Image(config.path)
this.iconGui = new ig.ImageGui(image, config.offsetX, config.offsetY, config.sizeX, config.sizeY)
this.iconGui.setPos(2, 8)
this.addChildGui(this.iconGui)
})
if (mod.stars !== undefined) {
this.starCount = new sc.TextGui(`${mod.stars}\\i[save-star]`)
// this.starCount.setAlign(ig.GUI_ALIGN.X_RIGHT, ig.GUI_ALIGN.Y_TOP)
this.starCount.setPos(517 - this.starCount.hook.size.x, 0) //this.versionText.hook.size.x + 4, 3)
this.addChildGui(this.starCount)
}

this.highlight = new ModListEntryHighlight(this.hook.size.x, this.hook.size.y, this.nameText.hook.size.x, buttonSquareSize * 3)
this.highlight.setPos(iconOffset, 0)
this.addChildGui(this.highlight)
this.addChildGui(this.nameText)
this.addChildGui(this.description)
this.addChildGui(this.versionText)
this.onButtonPress = () => this.askInstall()
},
updateDrawables(root) {
if (this.modList.hook.currentStateName != 'HIDDEN') {
Expand All @@ -122,6 +137,30 @@ export const ModListEntry: ModListEntryConstructor = ig.FocusGui.extend({
this.parent()
this.highlight.focus = this.focus
},
askInstall() {
sc.Dialogs.showChoiceDialog(
ig.lang.get('sc.gui.menu.ccmodloader.areYouSure').replace(/\[modName\]/, this.mod.name),
sc.DIALOG_INFO_ICON.QUESTION,
[
ig.lang.get('sc.gui.dialogs.no'),
ig.LangLabel.getText({
en_US: '[nods]',
de_DE: '[nickt]',
zh_CN: '[\u70b9\u5934]<<A<<[CHANGED 2017/10/10]',
ko_KR: '[\ub044\ub355]<<A<<[CHANGED 2017/10/17]',
ja_JP: '[\u3046\u306a\u305a\u304f]<<A<<[CHANGED 2017/11/04]',
zh_TW: '[\u9ede\u982d]<<A<<[CHANGED 2017/10/10]',
langUid: 13455,
}),
],
button => {
const resp = button?.text
if (resp == '[nods]') {
databases[this.mod.database].downloadMod(this.mod.id)
}
}
)
},
})

// this.openModSettingsButton = new sc.ButtonGui('\\i[mod-config]', buttonSquareSize - 1, true, this.modEntryActionButtons)
Expand Down
88 changes: 66 additions & 22 deletions src/gui/list.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import { ModEntry } from '../types'
import { ModListEntry } from './list-entry'
import { ModDB } from '../moddb'
import { databases } from '../moddb'
import { Fliters, createFuzzyFilteredModList } from '../filters'
import { SORT_ORDER } from './menu'

export interface ModMenuList extends sc.ListTabbedPane {
database: ModDB
mods: ModEntry[]
export interface ModMenuList extends sc.ListTabbedPane, sc.Model.Observer {
mods: Record<string, ModEntry[]>
filters: Fliters
tabz: { name: string; populateFunc: (list: sc.ButtonListBox, buttonGroup: sc.ButtonGroup) => void }[]
tabz: { name: string; icon: string; populateFunc: (list: sc.ButtonListBox, buttonGroup: sc.ButtonGroup, sort: SORT_ORDER) => void }[]
currentSort: SORT_ORDER

setMods(this: this, mods: ModEntry[], dbName: string): void
reloadFilters(this: this): void
reloadEntries(this: this): void
populateStore(this: this, list: sc.ButtonListBox, buttonGroup: sc.ButtonGroup): void
sortModEntries(this: this, mods: ModEntry[], sort: SORT_ORDER): void
populateStore(this: this, list: sc.ButtonListBox, buttonGroup: sc.ButtonGroup, sort: SORT_ORDER): void
populateSettings(this: this, list: sc.ButtonListBox, buttonGroup: sc.ButtonGroup): void
}
interface ModMenuListConstructor extends ImpactClass<ModMenuList> {
new (database: ModDB): ModMenuList
new (): ModMenuList
}

export const modMenuListWidth = 552
const modMenuListHeight = 240
export const ModMenuList: ModMenuListConstructor = sc.ListTabbedPane.extend({
mods: [],
mods: {},
transitions: {
DEFAULT: { state: {}, time: 0.2, timeFunction: KEY_SPLINES.LINEAR },
HIDDEN: { state: { alpha: 0, offsetX: 218 }, time: 0.2, timeFunction: KEY_SPLINES.LINEAR },
HIDDEN_EASE: { state: { alpha: 0, offsetX: 218 }, time: 0.2, timeFunction: KEY_SPLINES.EASE },
},
init(database: ModDB) {
this.database = database
init() {
this.parent(false)

this.tabz = [
{ name: 'Store', populateFunc: this.populateStore },
{ name: 'Settings', populateFunc: this.populateSettings },
{ name: 'Store', populateFunc: this.populateStore, icon: 'quest-all' },
{ name: 'Settings', populateFunc: this.populateSettings, icon: 'quest' },
]
this.filters = {}
this.currentSort = this.onInitSortType()

this.setAlign(ig.GUI_ALIGN.X_LEFT, ig.GUI_ALIGN.Y_TOP)
this.setSize(modMenuListWidth, modMenuListHeight)
Expand All @@ -46,11 +49,17 @@ export const ModMenuList: ModMenuListConstructor = sc.ListTabbedPane.extend({
this.addTab(this.tabz[i].name, i, {})
}

this.database.getMods().then(mods => {
this.mods = mods
this.reloadEntries()
})
//.catch(err => sc.Dialogs.showErrorDialog(err.message))
for (const dbName in databases) {
const db = databases[dbName]
if (db.active) {
this.mods[dbName] = []
db.getMods(dbName, mods => this.setMods(mods, dbName))
}
}
},
setMods(mods, dbName) {
this.mods[dbName] = mods
this.reloadEntries()
},
show() {
this.parent()
Expand All @@ -75,8 +84,12 @@ export const ModMenuList: ModMenuListConstructor = sc.ListTabbedPane.extend({
reloadEntries() {
this.setTab(this.currentTabIndex, true, { skipSounds: true })
},
onInitSortType() {
return SORT_ORDER.STARS
},
onTabButtonCreation(key: string, _index: number, settings) {
const button = new sc.ItemTabbedBox.TabButton(` ${key}`, `${key}`, 85)
const icon = this.tabz.find(tab => tab.name == key)!.icon
const button = new sc.ItemTabbedBox.TabButton(key, icon, 85)
button.textChild.setPos(7, 1)
button.setPos(0, 2)
button.setData({ type: settings.type })
Expand Down Expand Up @@ -105,19 +118,50 @@ export const ModMenuList: ModMenuListConstructor = sc.ListTabbedPane.extend({
}
list.addButton(repositoriesButton)
},
populateStore(list) {
const mods = createFuzzyFilteredModList(this.filters, this.mods)
sortModEntries(mods, sort) {
if (!this.filters.name) {
if (sort == SORT_ORDER.NAME) {
mods.sort((a, b) => a.name.localeCompare(b.name))
} else if (sort == SORT_ORDER.NAME_REVERSE) {
mods.sort((a, b) => a.name.localeCompare(b.name) * -1)
} else if (sort == SORT_ORDER.STARS) {
mods.sort((a, b) => (b.stars ?? -100) - (a.stars ?? -100))
}
}
},
populateStore(list, _, sort: SORT_ORDER) {
const mods = createFuzzyFilteredModList(
this.filters,
Object.values(this.mods).reduce((acc, v) => {
acc.push(...v)
return acc
}, [])
)
this.sortModEntries(mods, sort)
for (const mod of mods) {
const newModEntry = new ModListEntry(mod, this)
list.addButton(newModEntry)
}
},
onCreateListEntries(list, buttonGroup, _type, _sort) {
onCreateListEntries(list, buttonGroup) {
list.clear()
buttonGroup.clear()
this.tabz[this.currentTabIndex].populateFunc.bind(this)(list, buttonGroup)
this.tabz[this.currentTabIndex].populateFunc.bind(this)(list, buttonGroup, this.currentSort)
},
onListEntryPressed(_button) {
sc.BUTTON_SOUND.submit.play()
},
addObservers() {
sc.Model.addObserver(sc.menu, this)
},
removeObservers() {
sc.Model.removeObserver(sc.menu, this)
},
modelChanged(model, message, data) {
if (model == sc.menu && message == sc.MENU_EVENT.SORT_LIST) {
const sort = ((data as sc.ButtonGui).data as any).sortType as SORT_ORDER
this.currentSort = sort
this.reloadEntries()
}
},
})
Loading

0 comments on commit aa99222

Please sign in to comment.