The official website for the open-source compatibility layer fpPS4
1// this is my attempt on rewriting this... thing
2let avifSupport = false;
3let imageLoading = true;
4let oldestFilter = false;
5let datesButton = false;
6let statusFilter = [];
7let currentPage = 1;
8
9let issuesPerPage = 20;
10const codeRegex = /[a-zA-Z]{4}[0-9]{5}/;
11
12let Timer;
13let totalPages;
14let totalIssues;
15let fancyJsonData; // :3
16
17// Check Avif Support
18console.log(`Hey there! I've implemented an avif support check to spare browsers like Edge from having a stroke :D!`);
19const avif = new Image();
20avif.src = "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=";
21avif.onload = function() {
22 console.log('AVIF IS SUPPORTED :D');
23 avifSupport = true;
24};
25avif.onerror = function() {
26 console.log('AVIF IS NOT SUPPORTED D:');
27 avifSupport = false;
28 // window.alert("Hey! Your browser doesn't support avif, avif is an image format that has low file sizes while having high quality images. You won't get any game images");
29 const iButton = document.getElementById('imageButton');
30 console.log(iButton);
31 iButton.classList.add('selected');
32 iButton.style.cursor = 'default';
33 iButton.removeAttribute("onclick");
34};
35
36
37
38// game cards and stats handler and other on-load stuff
39document.addEventListener('DOMContentLoaded', async function () {
40 await init()
41
42 // + check cookies
43 const imageLoadingCookie = checkCookies("imageLoadingSetting", "true");
44 const datesSetting = checkCookies("datesSetting", "false");
45
46 if (imageLoadingCookie === "false") {
47 imageLoading = false;
48 document.getElementById("imageButton").classList.add('selected');
49 }
50
51 if (datesSetting === "true") {
52 datesButton = true;
53 document.getElementById('datesButton').classList.toggle('selected');
54 }
55
56 // Adjust screen size for mobile and 4k monitors for some reason
57 // window.addEventListener('load', adjustScreenSize);
58 window.addEventListener('resize', adjustScreenSize);
59
60
61 // + fetch issues and set the tag bars
62 fetch('https://api.fpps4.net/database.json')
63 .then(response => response.json())
64 .then(jsonData => {
65 fancyJsonData = jsonData;
66
67 totalIssues = jsonData.length;
68 totalPages = Math.ceil(totalIssues / issuesPerPage);
69
70 console.log("\nCOMPATIBILITY STATS");
71
72 let availableStatus = ['Nothing', 'Boots', 'Menus', 'Ingame', 'Playable'];
73 let totalPercentage = 0;
74
75 let statusPercentages = [];
76 let statusCount = [];
77
78 availableStatus.forEach(status => {
79 statusCount[status] = 0; // init tag count
80
81 jsonData.forEach(issue => {
82 if (issue.status === status) {
83 statusCount[status]++;
84 }
85 });
86
87 let rawPercentage = parseFloat((statusCount[status] / totalIssues * 100).toFixed(2));
88
89 statusPercentages[status] = rawPercentage;
90 totalPercentage += rawPercentage;
91 });
92
93 // stats & tag filter
94 availableStatus.forEach(status => {
95 let percent = statusPercentages[status];
96 let count = statusCount[status];
97 let element = document.getElementById(status + 'Bar');
98 let textElement = document.getElementById(status + 'Info')
99 let parentElement = element.parentElement;
100 console.log(`${status} = ${percent}% [${count}]`);
101 element.style.width = percent + '%';
102 textElement.textContent = percent + '% - ' + count;
103
104 parentElement.addEventListener('click', function () {
105 parentElement.classList.toggle('selected');
106 statusFilter.includes(status) ? statusFilter.splice(statusFilter.indexOf(status), 1) : statusFilter.push(status);
107 currentPage = 1;
108 updateSearchResults();
109 });
110 });
111 console.log("\n");
112
113 gameCardHandler(jsonData.slice(0, issuesPerPage)); //first 20
114 pageButtonHandler();
115
116 })
117 .catch(console.error);
118});
119
120
121// Searching
122document.querySelector('#search').addEventListener('input', function() {
123 currentPage = 1;
124 updateSearchResults();
125});
126
127function updateSearchResults() {
128 clearTimeout(Timer);
129 const gameWrapper = document.querySelector('#gameWrapper');
130 const searchQuery = document.querySelector('#search').value.toLowerCase();
131
132 gameWrapper.querySelectorAll('.gameContainer').forEach(container => {
133 const skeletonDiv = document.createElement('div');
134 skeletonDiv.classList.add('gameContainer', 'skeletonAnimation');
135 gameWrapper.replaceChild(skeletonDiv, container);
136 });
137
138 Timer = setTimeout(() => {
139 let jsonData = [];
140 let isCodeSearch = codeRegex.test(searchQuery);
141
142 fancyJsonData.forEach(issue => {
143 // id based searching
144 if (isCodeSearch && (issue.code.toLowerCase() !== searchQuery)) {
145 return;
146
147 // title based searching
148 } else if (!isCodeSearch && !issue.title.toLowerCase().includes(searchQuery)) {
149 return;
150 }
151
152 // filter tags
153 if (statusFilter.length > 0) {
154 let isGood = false;
155
156 for (const status of statusFilter) {
157 if (status === issue.status) {
158 isGood = true;
159 break;
160 }
161 }
162
163 if (isGood === false) {
164 return;
165 }
166 }
167
168 jsonData.push(issue);
169 });
170
171
172 let startSlice = (currentPage - 1) * issuesPerPage; // makes it start on 0
173 let endSlice = startSlice + issuesPerPage;
174
175 totalPages = Math.ceil(jsonData.length / issuesPerPage);
176 totalIssues = jsonData.length;
177
178 let tempJsonData;
179
180 if (oldestFilter === true) {
181 tempJsonData = jsonData.reverse();
182 } else {
183 tempJsonData = jsonData;
184 }
185
186 gameCardHandler(tempJsonData.slice(startSlice, endSlice));
187 pageButtonHandler();
188
189 }, 300);
190}
191
192// Game Card handler
193function gameCardHandler(jsonData) {
194 const gameWrapper = document.getElementById("gameWrapper");
195 gameWrapper.innerHTML = "";
196
197 jsonData.forEach(issue => {
198 // game image URL
199 let imageSource = "";
200 let imageText = "GAME";
201 let imageTextSize = 1.25;
202
203 switch(true) {
204 case issue.image && imageLoading && avifSupport && issue.issue_type === "Homebrew":
205 imageSource = "https://api.fpps4.net/images/homebrew/" + issue.title + ".avif";
206 break;
207 case issue.image && imageLoading && avifSupport:
208 imageSource = "https://api.fpps4.net/images/game/" + issue.code +".avif";
209 break;
210 case issue.issue_type === "SystemFwUnknown" || issue.issue_type === "SystemFw505":
211 imageText = "SYSTEM";
212 imageTextSize = 1.13;
213 break
214 }
215
216 if (issue.issue_type === "Homebrew") { // needs to be applied to all homebrews
217 imageText = "HOME<br>BREW";
218 imageTextSize = 1.25;
219 if (issue.code === "") {
220 issue.code = "HOMEBREW";
221 }
222 }
223
224 let imageTextEnabled = issue.image && imageLoading && avifSupport ? "none" : "flex";
225 let updated = new Date(issue.updated).toLocaleDateString()
226
227
228 // game cards
229 const gameElementHTML = `
230 <div class="gameContainer">
231 <a class="gameImageLink" target="_blank" href="https://github.com/red-prig/fpps4-game-compatibility/issues/${issue.id}">
232 <p class="gameImageText" style="font-size: ${imageTextSize}rem; display: ${imageTextEnabled};">${imageText}</p>
233 ${imageSource ? `<img class="gameImage" loading="lazy" alt="${issue.title} - ${issue.code} game image" src="${imageSource}">` : "" }
234 </a>
235 <div class="gameSeparator ${issue.status}"></div>
236 <div class="gameDetails">
237 <p class="gameName">${issue.title}</p>
238 <p class="gameCusa" data-date="${updated}" data-cusa="${issue.code}">${issue.code}</p>
239 <p class="gameStatus ${issue.status}">${issue.status}</p>
240 </div>
241 </div>`;
242
243 const tempContainer = document.createElement('div');
244 tempContainer.innerHTML = gameElementHTML;
245 const gameContainer = tempContainer.querySelector('.gameContainer');
246 gameWrapper.appendChild(gameContainer);
247 });
248
249 const statContainer = document.createElement('h4');
250 statContainer.innerHTML = `<h4 class="totalTimeText">${totalIssues} results </h4>`;
251 gameWrapper.appendChild(statContainer);
252
253 if (datesButton) {
254 dateButtonHandler();
255 }
256
257 // Image Effect
258 document.querySelectorAll('.gameImage, .gameImageText').forEach(image => {
259 image.addEventListener('mousemove', e => {
260 const r = image.getBoundingClientRect();
261 const x = e.clientX - r.left;
262 const y = e.clientY - r.top;
263 image.style.transformOrigin = `${x}px ${y}px`;
264 image.style.transform = 'scale(1.08)';
265 });
266 image.addEventListener('mouseleave', () => {
267 image.style.transform = 'scale(1)';
268 });
269 });
270
271 // functions in required.js
272 updateFooter();
273}
274
275
276// NoImage button
277function imageButton(button) {
278 imageLoading = !imageLoading;
279 setCookie("imageLoadingSetting", imageLoading);
280 button.classList.toggle('selected');
281 imageButtonHandler();
282}
283
284
285// Date Button
286function dateButton(button) {
287 datesButton = !datesButton;
288 setCookie("datesSetting", datesButton);
289 button.classList.toggle('selected');
290 dateButtonHandler();
291}
292
293
294// Oldes/Newest Button
295function sortButton(button) {
296 oldestFilter = !oldestFilter;
297 button.classList.toggle('selected');
298 updateSearchResults();
299}
300
301
302function imageButtonHandler() {
303 if (imageLoading === false) {
304 document.querySelectorAll(".gameImageText").forEach(imageText => {
305 imageText.style.display = "flex";
306 });
307 } else {
308 updateSearchResults();
309 }
310}
311
312
313function dateButtonHandler() {
314 document.querySelectorAll(".gameCusa").forEach(element => {
315 if (datesButton === true) {
316 element.textContent = element.dataset.date;
317 } else {
318 element.textContent = element.dataset.cusa;
319 }
320 });
321}
322
323function pageButtonHandler(type) {
324 // maybe make it handle everything in once?
325 const maxNumber = totalPages;
326 const searchBar = document.getElementById("search3");
327 const minButton = document.getElementById("pageBarMin");
328 const maxButton = document.getElementById("pageBarMax");
329
330 maxButton.textContent = maxNumber;
331
332 if (currentPage != maxNumber) {
333 if (type === "forward") {
334 currentPage += 1;
335 updateSearchResults();
336 } else if (type === "max") {
337 currentPage = maxNumber;
338 updateSearchResults();
339 }
340 }
341
342 if (currentPage != 1) {
343 if (type === "back") {
344 currentPage -= 1;
345 updateSearchResults();
346 } else if (type === "min") {
347 currentPage = 1;
348 updateSearchResults();
349 }
350 }
351
352 if (type === "input") {
353 clearTimeout(Timer);
354
355 Timer = setTimeout(() => {
356 inputValue = parseInt(searchBar.value);
357 console.log(inputValue);
358
359 if (inputValue > maxNumber) {
360 inputValue = maxNumber;
361 } else if (inputValue < 1) {
362 inputValue = 1;
363 }
364
365 currentPage = inputValue;
366 searchBar.placeholder = currentPage;
367 searchBar.value = "";
368 updateSearchResults();
369 }, 400);
370 }
371
372
373
374 if (currentPage !== 1 && currentPage !== maxNumber) {
375 searchBar.placeholder = currentPage;
376 searchBar.classList.add("selected");
377 maxButton.classList.remove("selected");
378 minButton.classList.remove("selected");
379 } else if (currentPage === 1) {
380 minButton.classList.add("selected");
381 maxButton.classList.remove("selected");
382 searchBar.classList.remove("selected");
383 searchBar.placeholder = "...";
384 } else if (currentPage === maxNumber) {
385 minButton.classList.remove("selected");
386 maxButton.classList.add("selected");
387 searchBar.classList.remove("selected");
388 searchBar.placeholder = "...";
389 }
390}
391
392search3.addEventListener("click", function() {
393 if (search3.placeholder == '...') {
394 search3.placeholder = '';
395 }
396});
397
398search3.addEventListener("blur", function() {
399 if (search3.placeholder == '') {
400 search3.placeholder = '...';
401 }
402});