Skip to content

Commit c67f1fd

Browse files
committed
Expands CurseForge changelog history options
Previously, only recent CurseForge mod file changelogs were accessible due to limitations of the `latestFiles` API. This change implements paginated fetching of a more comprehensive list of mod files, allowing users to view changelogs for older versions. The UI gracefully falls back to displaying `latestFiles` while the full list loads, ensuring a smooth user experience.
1 parent d9d384a commit c67f1fd

1 file changed

Lines changed: 88 additions & 20 deletions

File tree

  • apps/desktop/packages/mainWindow/src/pages/AddonViewPage

apps/desktop/packages/mainWindow/src/pages/AddonViewPage/Changelog.tsx

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,80 @@ const Changelog = () => {
168168
const rspcContext = rspc.useContext()
169169

170170
const routeData = useChangelogData()
171-
const lastFile = () =>
172-
routeData.isCurseforge &&
173-
routeData.modpackDetails?.data?.data.latestFiles[
174-
routeData.modpackDetails?.data?.data.latestFiles.length - 1
175-
]
176171

177172
const [options, setOptions] = createSignal<string[]>([])
178173
const [optionLabels, setOptionLabels] = createSignal<Record<string, string>>(
179174
{}
180175
)
176+
const [cfFiles, setCfFiles] = createSignal<CFFEFile[]>([])
177+
const [cfFilesLoading, setCfFilesLoading] = createSignal(
178+
routeData.isCurseforge
179+
)
181180
const [fileId, setFileId] = createSignal<number | string | undefined>(
182181
undefined
183182
)
183+
184+
// CurseForge's `latestFiles` only contains a handful of recent files, which made
185+
// older versions' changelogs unreachable. Page through the full files list instead;
186+
// `latestFiles` remains only as a fallback if that fetch fails.
187+
createAsyncEffect((isStale) => {
188+
if (!routeData.isCurseforge) return
189+
const modpackId = parseInt(params.id, 10)
190+
191+
// Don't leak the previous addon's files into the selector while refetching
192+
setCfFiles([])
193+
setCfFilesLoading(true)
194+
195+
const PAGE_SIZE = 50
196+
const MAX_FILES = 1000
197+
198+
const all: CFFEFile[] = []
199+
200+
const fetchAll = async () => {
201+
while (all.length < MAX_FILES) {
202+
const response = await rspcContext.client.query([
203+
"modplatforms.curseforge.getModFiles",
204+
{
205+
modId: modpackId,
206+
query: { index: all.length, pageSize: PAGE_SIZE }
207+
}
208+
])
209+
210+
if (isStale()) return
211+
all.push(...response.data)
212+
213+
const total = response.pagination?.totalCount
214+
if (
215+
response.data.length < PAGE_SIZE ||
216+
(total != null && all.length >= total)
217+
) {
218+
break
219+
}
220+
}
221+
222+
if (isStale()) return
223+
setCfFiles(all)
224+
setCfFilesLoading(false)
225+
}
226+
227+
fetchAll().catch((e) => {
228+
console.error(e)
229+
if (isStale()) return
230+
231+
// Failure fallback: whatever pages were fetched plus the `latestFiles` subset,
232+
// so the selector is still usable
233+
const fallback = [...all]
234+
for (const file of routeData.modpackDetails.data?.data.latestFiles ||
235+
[]) {
236+
if (!fallback.some((f) => f.id === file.id)) {
237+
fallback.push(file)
238+
}
239+
}
240+
241+
setCfFiles(fallback)
242+
setCfFilesLoading(false)
243+
})
244+
})
184245
const [changeLog, setChangelog] = createSignal<string | undefined>(undefined)
185246
const [releaseDate, setReleaseDate] = createSignal<string | undefined>(
186247
undefined
@@ -210,20 +271,32 @@ const Changelog = () => {
210271
setOptionLabels(labels)
211272
}
212273
} else {
213-
// Use latestFiles for actual distinct file versions (not latestFilesIndexes which is per-game-version)
214-
const files = routeData.modpackDetails.data?.data.latestFiles || []
215-
setChangelog(undefined)
216-
setReleaseDate(undefined)
217-
setIsLoadingChangelog(false)
274+
if (cfFilesLoading()) {
275+
// Keep the skeleton up until the full files list is in
276+
setFileId(undefined)
277+
setChangelog(undefined)
278+
setReleaseDate(undefined)
279+
setIsLoadingChangelog(true)
280+
setOptions([])
281+
setOptionLabels({})
282+
return
283+
}
284+
285+
const files = [...cfFiles()].sort(
286+
(a, b) =>
287+
new Date(b.fileDate).getTime() - new Date(a.fileDate).getTime()
288+
)
218289

219290
const opts = files.map((file) => file.id.toString())
220291
const labels = Object.fromEntries(
221292
files.map((file) => [file.id.toString(), file.displayName])
222293
)
223294

224-
// Set default value to first option for CurseForge
225-
if (opts.length > 0) {
295+
const current = fileId()?.toString()
296+
if (opts.length > 0 && (!current || !opts.includes(current))) {
226297
setFileId(opts[0])
298+
} else if (opts.length === 0) {
299+
setIsLoadingChangelog(false)
227300
}
228301

229302
setOptions(opts)
@@ -234,21 +307,15 @@ const Changelog = () => {
234307
createAsyncEffect((isStale) => {
235308
const modpackId = parseInt(params.id, 10)
236309
const currentFileId = fileId()
237-
const currentLastFile = lastFile()
238310
const isCurseforge = routeData.isCurseforge
239311

240312
if (isCurseforge) {
241-
if (
242-
currentFileId !== undefined ||
243-
(currentLastFile && currentLastFile.id !== undefined)
244-
) {
313+
if (currentFileId !== undefined) {
245314
setIsLoadingChangelog(true)
246315
setChangelog(undefined)
247316
setReleaseDate(undefined)
248317

249-
const targetFileId =
250-
parseInt(currentFileId as string, 10) ||
251-
(currentLastFile as CFFEFile).id
318+
const targetFileId = parseInt(currentFileId as string, 10)
252319

253320
rspcContext.client
254321
.query([
@@ -263,6 +330,7 @@ const Changelog = () => {
263330
if (!isStale()) {
264331
setChangelog(changelogQuery.data)
265332
const fileData =
333+
cfFiles().find((file) => file.id === targetFileId) ||
266334
routeData.modpackDetails.data?.data.latestFiles.find(
267335
(file) => file.id === targetFileId
268336
) ||

0 commit comments

Comments
 (0)