@@ -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