// ==UserScript==
// @name Enhanced BLAEO
// @namespace https://rafaelgssa.gitlab.io/monkey-scripts
// @version 5.0.2
// @author rafaelgssa
// @description Adds some cool features to BLAEO.
// @match https://www.backlog-assassins.net/*
// @match https://www.steamgifts.com/discussion/9VTBD/*
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js?version=821710
// @require https://greasyfork.org/scripts/405802-monkey-dom/code/Monkey%20DOM.js?version=823982
// @require https://greasyfork.org/scripts/405831-monkey-storage/code/Monkey%20Storage.js?version=821709
// @require https://greasyfork.org/scripts/405822-monkey-requests/code/Monkey%20Requests.js?version=821708
// @require https://greasyfork.org/scripts/406057-blaeo-api/code/BLAEO%20API.js?version=823678
// @connect steamgifts.com
// @connect steamcommunity.com
// @run-at document-idle
// @grant GM.info
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.xmlHttpRequest
// @grant GM.openInTab
// @grant GM_info
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @noframes
// ==/UserScript==
/* global BlaeoApi, DOM, PersistentStorage, Requests, Utils */
/**
* @typedef {'new' | GameProgress} GameCategory
*
* @typedef {'uncategorized' | 'never-played' | 'unfinished' | 'beaten' | 'completed' | 'wont-play'} GameProgress
*
* @typedef {Object} GameCategoryInfo
* @property {string} name
* @property {string} color
* @property {string} bootstrapClass
*
* @typedef {Object} EblaeoGlobals Global variables for the entire script.
* @property {UserData} user
* @property {Partial<SmGlobals>} sm
* @property {Partial<GlcGlobals>} glc
* @property {Partial<PgGlobals>} pg
* @property {HTMLElement | null} alertEl
* @property {HTMLButtonElement | null} dialogModalButton
* @property {HTMLElement | null} dialogModalHolderEl
* @property {HTMLElement | null} dialogModalEl
* @property {HTMLElement | null} dialogModalLabelEl
* @property {HTMLElement | null} dialogModalFooterEl
* @property {HTMLElement | null} dialogModalDenyButton
* @property {HTMLElement | null} dialogModalConfirmButton
*
* @typedef {Object} UserData
* @property {string} steamId
* @property {string} username
* @property {boolean} isAdmin
* @property {Record<number, OwnedGame>} ownedGames
* @property {number} lastSync
* @property {Record<string, number[]>} glcLists
* @property {Record<string, PgPreset<PgPresetType>>} pgPresets
*
* @typedef {Object} OwnedGame
* @property {number} id
* @property {GameProgress} [progress]
*
* @typedef {Object} SmGlobals Global variables for Settings Menu.
* @property {HTMLElement | null} sidebarNavEl
* @property {HTMLElement | null} button
* @property {HTMLElement | null} mainContainerEl
* @property {HTMLButtonElement | null} syncButton
* @property {HTMLElement | null} lastSyncEl
* @property {HTMLElement | null} usernameEl
* @property {HTMLElement | null} ownedGamesEl
*
* @typedef {Object} GlcGlobals Global variables for Game List Checker.
* @property {HTMLElement | null} postEl
* @property {HTMLElement[]} listItemEls
* @property {HTMLButtonElement | null} button
* @property {HTMLElement | null} buttonIconEl
* @property {HTMLButtonElement | null} modalButton
* @property {HTMLElement | null} modalEl
* @property {HTMLElement | null} modalBodyEl
* @property {boolean} hasChecked
* @property {boolean} isFirstCheck
* @property {Partial<Record<GameCategory, GlcGame[]>>} games
* @property {number} gamesCount
* @property {string} postId
*
* @typedef {Object} GlcGame
* @property {number} id
* @property {string} name
* @property {boolean} [isNew]
*
* @typedef {Object} PgGlobals Global variables for Post Generator.
* @property {HTMLTextAreaElement | null} postField
* @property {HTMLElement | null} previewButton
* @property {HTMLElement | null} createButton
* @property {Record<string, PgPreset<PgPresetType>>} defaultPresets
* @property {PgGame} defaultGame
* @property {PgPresetType[]} presetTypes
* @property {Record<PgPresetType, string>} presetTypeNames
* @property {Record<PgPresetType, PgPreset<PgPresetType>>} presets
* @property {PgPresetType} currentPresetType
* @property {PgGameInfo[]} gameInfos
* @property {PgGame | null} selectedGame
* @property {boolean} isEditing
* @property {Partial<Record<PgPresetType, HTMLElement | null>>} presetTabNavEls
* @property {Partial<Record<PgPresetType, HTMLElement | null>>} presetTabEls
* @property {Partial<Record<PgPresetType, HTMLElement | null>>} presetDropdownEls
* @property {Pick<PgFieldContainers, 'presets'> & Partial<PgFieldContainers>} fieldContainerEls
* @property {Pick<PgFields, 'presets'> & Partial<PgFields>} fields
* @property {boolean} isSearchingGames
* @property {boolean} hasNewGameSearchQuery
* @property {HTMLElement | null} generateButton
* @property {HTMLElement | null} modalEl
* @property {HTMLElement | null} modalBodyEl
* @property {HTMLInputElement | null} searchGamesField
* @property {HTMLElement | null} searchGamesResultsEl
* @property {HTMLElement | null} generatorEl
* @property {HTMLElement | null} generatorBodyEl
* @property {HTMLElement | null} generatorNavEl
* @property {HTMLElement | null} savePresetButton
* @property {HTMLElement | null} gamePreviewContainerEl
* @property {HTMLElement | null} gamePreviewEl
* @property {HTMLElement | null} gamePreviewBodyEl
* @property {HTMLElement | null} gamePreviewButton
* @property {HTMLElement | null} fullPreviewEl
* @property {HTMLElement | null} fullPreviewBodyEl
* @property {HTMLElement | null} doneButton
* @property {PgGame[]} gamesCache
* @property {Record<number, number>} playtimeThisMonthCache
* @property {Record<number, number>} screenshotsCache
* @property {Record<number, PgReviewCache>} reviewsCache
*
* @typedef {'box' | 'bar' | 'panel' | 'custom'} PgPresetType
*
* @typedef {Object} PgPresets
* @property {PgBoxPreset} box
* @property {PgBarPreset} bar
* @property {PgPanelPreset} panel
* @property {PgCustomPreset} custom
*
* @typedef {PgPresetBase & PgBoxPresetBase} PgBoxPreset
*
* @typedef {PgPresetBase & PgBarPresetBase} PgBarPreset
*
* @typedef {PgPresetBase & PgPanelPresetBase} PgPanelPreset
*
* @typedef {PgCustomPresetBase} PgCustomPreset
*
* @typedef {Object} PgPresetBase
* @property {boolean} showPlaytimeThisMonth
* @property {string} playtimeTemplate
* @property {boolean} linkAchievements
* @property {string} achievementsTemplate
* @property {string} noAchievementsTemplate
* @property {boolean} checkScreenshots
* @property {boolean} linkScreenshots
* @property {string} screenshotsTemplate
* @property {string} noScreenshotsTemplate
* @property {'Solid' | 'Horizontal gradient' | 'Vertical gradient'} bgType
* @property {string} bgColor1
* @property {string} bgColor2
* @property {string} titleColor
* @property {string} textColor
* @property {string} linkColor
*
* @typedef {Object} PgBoxPresetBase
* @property {'Left' | 'Right'} reviewPosition
*
* @typedef {Object} PgBarPresetBase
* @property {boolean} showInfoInOneLine
* @property {'Left' | 'Right' | 'Hidden'} completionBarPosition
* @property {'Left' | 'Right'} imagePosition
* @property {boolean} useCollapsibleReview
* @property {'Bar click' | 'Button click'} reviewTriggerMethod
*
* @typedef {Object} PgPanelPresetBase
* @property {boolean} usePredefinedTheme
* @property {'Blue' | 'Green' | 'Grey' | 'Red' | 'Yellow'} predefinedThemeColor
* @property {boolean} useCustomTheme
* @property {boolean} useCollapsibleReview
*
* @typedef {Object} PgCustomPresetBase
* @property {string} htmlTemplate
*
* @typedef {Object} PgGame
* @property {number} id
* @property {string} name
* @property {string} image
* @property {GameProgress} progress
* @property {PgGamePlaytime} playtime
* @property {PgGameAchievements} achievements
* @property {number} screenshotsCount
* @property {string} customHtml
* @property {string} rating
* @property {string} review
* @property {PgPreset<PgPresetType>} preset
*
* @typedef {Object} PgGamePlaytime
* @property {number} thisMonth
* @property {number} total
*
* @typedef {Object} PgGameAchievements
* @property {number} unlocked
* @property {number} total
*
* @typedef {Object} PgGameInfo
* @property {PgGame} game
* @property {ElementArray[]} elArrays
*
* @typedef {Record<'presets', PgPresetFieldContainers> & PgFieldContainersBase} PgFieldContainers
*
* @typedef {Object} PgPresetFieldContainers
* @property {Partial<PgBoxFieldContainers>} box
* @property {Partial<PgBarFieldContainers>} bar
* @property {Partial<PgPanelFieldContainers>} panel
* @property {Partial<PgCustomFieldContainers>} custom
*
* @typedef {PgPresetFieldContainersBase & PgBoxFieldContainersBase} PgBoxFieldContainers
*
* @typedef {PgPresetFieldContainersBase & PgBarFieldContainersBase} PgBarFieldContainers
*
* @typedef {PgPresetFieldContainersBase & PgPanelFieldContainersBase} PgPanelFieldContainers
*
* @typedef {PgCustomFieldContainersBase} PgCustomFieldContainers
*
* @typedef {Record<keyof PgPresetFieldsBase, HTMLElement | null>} PgPresetFieldContainersBase
*
* @typedef {Record<keyof PgBoxFieldsBase, HTMLElement | null>} PgBoxFieldContainersBase
*
* @typedef {Record<keyof PgBarFieldsBase, HTMLElement | null>} PgBarFieldContainersBase
*
* @typedef {Record<keyof PgPanelFieldsBase, HTMLElement | null>} PgPanelFieldContainersBase
*
* @typedef {Record<keyof PgCustomFieldsBase, HTMLElement | null>} PgCustomFieldContainersBase
*
* @typedef {Record<PgFieldKey, HTMLElement | null>} PgFieldContainersBase
*
* @typedef {Object} PgFields
* @property {PgPresetFields} presets
* @property {HTMLInputElement | null} customHtml
* @property {HTMLInputElement | null} rating
* @property {HTMLTextAreaElement | null} review
* @property {HTMLInputElement | null} presetName
*
* @typedef {Object} PgPresetFields
* @property {Partial<PgBoxFields>} box
* @property {Partial<PgBarFields>} bar
* @property {Partial<PgPanelFields>} panel
* @property {Partial<PgCustomFields>} custom
*
* @typedef {PgPresetFieldsBase & PgBoxFieldsBase} PgBoxFields
*
* @typedef {PgPresetFieldsBase & PgBarFieldsBase} PgBarFields
*
* @typedef {PgPresetFieldsBase & PgPanelFieldsBase} PgPanelFields
*
* @typedef {PgCustomFieldsBase} PgCustomFields
*
* @typedef {Object} PgPresetFieldsBase
* @property {HTMLInputElement | null} showPlaytimeThisMonth
* @property {HTMLInputElement | null} playtimeTemplate
* @property {HTMLInputElement | null} linkAchievements
* @property {HTMLInputElement | null} achievementsTemplate
* @property {HTMLInputElement | null} noAchievementsTemplate
* @property {HTMLInputElement | null} checkScreenshots
* @property {HTMLInputElement | null} linkScreenshots
* @property {HTMLInputElement | null} screenshotsTemplate
* @property {HTMLInputElement | null} noScreenshotsTemplate
* @property {HTMLInputElement | null} bgType
* @property {HTMLInputElement | null} bgColor1
* @property {HTMLInputElement | null} bgColor2
* @property {HTMLInputElement | null} titleColor
* @property {HTMLInputElement | null} textColor
* @property {HTMLInputElement | null} linkColor
*
* @typedef {Object} PgBoxFieldsBase
* @property {HTMLInputElement | null} reviewPosition
*
* @typedef {Object} PgBarFieldsBase
* @property {HTMLInputElement | null} showInfoInOneLine
* @property {HTMLInputElement | null} customHtml
* @property {HTMLInputElement | null} completionBarPosition
* @property {HTMLInputElement | null} imagePosition
* @property {HTMLInputElement | null} useCollapsibleReview
* @property {HTMLInputElement | null} reviewTriggerMethod
*
* @typedef {Object} PgPanelFieldsBase
* @property {HTMLInputElement | null} rating
* @property {HTMLInputElement | null} usePredefinedTheme
* @property {HTMLInputElement | null} predefinedThemeColor
* @property {HTMLInputElement | null} useCustomTheme
* @property {HTMLInputElement | null} useCollapsibleReview
*
* @typedef {Object} PgCustomFieldsBase
* @property {HTMLInputElement | null} htmlTemplate
*
* @typedef {Object} PgFieldOptions
* @property {'textarea' | 'text' | 'color' | 'checkbox' | 'radio' | 'select'} type
* @property {PgPresetFieldKey | PgFieldKey} id
* @property {string} htmlId
* @property {string} label
* @property {string} [description]
* @property {boolean} [usePlaceholders]
* @property {boolean} [useReviewPlaceholder]
* @property {string[]} [selectOptions]
*
* @typedef {keyof PgPresetBase | keyof PgBoxPresetBase | keyof PgBarPresetBase | keyof PgPanelPresetBase | keyof PgCustomPresetBase} PgPresetFieldKey
*
* @typedef {keyof Omit<PgFields, 'presets'>} PgFieldKey
*
* @typedef {Object} PgReviewCache
* @property {string} review
* @property {string} reviewPreview
*/
/**
* @template {PgPresetType} T
* @typedef {Object} PgPreset
* @property {T} type
* @property {string} name
* @property {PgPresets[T]} prefs
*/
class CustomError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message);
}
}
(async () => {
'use strict';
const scriptId = 'eblaeo';
const scriptName = GM.info.script.name;
/** @type {UserData} */
const defaultValues = {
steamId: '',
username: '',
isAdmin: false,
ownedGames: {},
lastSync: 0,
glcLists: {},
pgPresets: {},
};
const isInBlaeo = window.location.host === 'www.backlog-assassins.net';
const gameCategories = /** @type {GameCategory[]} */ ([
'new',
'uncategorized',
'never-played',
'unfinished',
'beaten',
'completed',
'wont-play',
]);
/** @type {Record<GameCategory, GameCategoryInfo>} */
const gameCategoryInfos = {
new: {
name: 'New',
color: '#555555',
bootstrapClass: 'primary',
},
uncategorized: {
name: 'Uncategorized',
color: '#dddddd',
bootstrapClass: 'default',
},
'never-played': {
name: 'Never Played',
color: '#eeeeee',
bootstrapClass: 'default',
},
unfinished: {
name: 'Unfinished',
color: '#f0ad4e',
bootstrapClass: 'warning',
},
beaten: {
name: 'Beaten',
color: '#5cb85c',
bootstrapClass: 'success',
},
completed: {
name: 'Completed',
color: '#5bc0de',
bootstrapClass: 'info',
},
'wont-play': {
name: "Won't Play",
color: '#d9534f',
bootstrapClass: 'danger',
},
};
/** @type {Record<BlaeoGameProgress, GameProgress>} */
const gameProgresses = {
uncategorized: 'uncategorized',
'never played': 'never-played',
unfinished: 'unfinished',
beaten: 'beaten',
completed: 'completed',
'wont play': 'wont-play',
};
const bootstrapColorClasses = {
Grey: 'default',
Yellow: 'warning',
Green: 'success',
Blue: 'info',
Red: 'danger',
};
const eblaeo = /** @type {EblaeoGlobals} */ ({
user: {},
sm: {},
glc: {},
pg: {},
});
/**
* Loads the script.
* @returns {Promise<void>}
*/
const load = async () => {
await loadUserData();
addStyles();
await loadFeatures();
if (isInBlaeo) {
document.addEventListener('turbolinks:load', loadFeatures);
}
};
/**
* Loads the user's data.
* @returns {Promise<void>}
*/
const loadUserData = async () => {
const keys = /** @type {(keyof UserData)[]} */ (Object.keys(defaultValues));
for (const key of keys) {
eblaeo.user[key] = /** @type {never} */ (await PersistentStorage.getValue(key));
}
};
/**
* Adds styles to the page.
*/
const addStyles = () => {
// prettier-ignore
DOM.insertElements(document.head, 'beforeend', [
['style', null,
isInBlaeo
? `
.clear-both {
clear: both;
}
#eblaeo-alert, #eblaeo-dialog-modal-button, #eblaeo-glc-modal-button, #eblaeo-pg-generator, #eblaeo-pg-full-preview, .eblaeo-pg-collapse, .eblaeo-pg-expand {
display: none;
}
#eblaeo-alert {
margin: 10px 0;
}
#eblaeo-glc-button-container, .eblaeo-pg-full-preview-game {
position: relative;
}
#eblaeo-glc-button {
height: 38px;
max-height: 38px;
max-width: 38px;
position: absolute;
right: -19px;
text-align: center;
top: 15px;
width: 38px;
}
#eblaeo-glc-button i {
vertical-align: middle;
}
.eblaeo-glc-results-section .panel-heading {
position: sticky;
top: 0;
}
#eblaeo-pg-generate-button {
margin-right: 5px;
}
#eblaeo-pg-modal {
overflow: auto;
}
.eblaeo-pg-game-list-item-button {
margin-bottom: 5px;
}
ul[id^='eblaeo-pg-preset-dropdown']:empty::after {
content: 'No presets saved for this type.';
padding: 5px;
}
ul[id^='eblaeo-pg-preset-dropdown'] li {
align-items: center;
display: flex;
}
ul[id^='eblaeo-pg-preset-dropdown'] li a {
cursor: pointer;
display: inline-block;
flex: 1;
}
ul[id^='eblaeo-pg-preset-dropdown'] li i {
cursor: pointer;
padding: 3px 10px;
}
.eblaeo-pg-field-container {
margin-bottom: 15px;
}
.eblaeo-pg-double-field-container, .eblaeo-pg-triple-field-container {
display: flex;
}
:not(.eblaeo-pg-double-field-container) > .eblaeo-pg-field-container select {
width: calc(50% - 15px);
}
.eblaeo-pg-double-field-container >* {
width: calc(50% - 15px);
}
.eblaeo-pg-double-field-container >:first-child {
margin-right: 15px;
}
.eblaeo-pg-double-field-container >:last-child {
margin-left: 15px;
}
.eblaeo-pg-triple-field-container >* {
width: calc(34% - 15px);
}
.eblaeo-pg-triple-field-container >:not(:last-child) {
margin-right: 15px;
}
#eblaeo-pg-game-preview-container {
background-color: #ffffff;
bottom: 0;
display: none;
padding: 15px 0;
position: sticky;
z-index: 999;
}
#eblaeo-pg-game-preview {
margin: 0;
max-height: 300px;
overflow: auto;
}
.eblaeo-pg-full-preview-game .btn-toolbar {
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
left: 0;
margin: 0;
padding: 5px 5px 5px 0;
position: absolute;
right: 0;
top: 0;
z-index: 2;
}
.eblaeo-pg-full-preview-game:hover .btn-toolbar {
display: flex;
}
#eblaeo-pg-game-preview .panel-heading.collapsed .eblaeo-pg-expand, #eblaeo-pg-game-preview .panel-heading:not(.collapsed) .eblaeo-pg-collapse, #eblaeo-pg-full-preview .panel-heading.collapsed .eblaeo-pg-expand, #eblaeo-pg-full-preview .panel-heading:not(.collapsed) .eblaeo-pg-collapse {
display: block;
}
.eblaeo-pg-collapse, .eblaeo-pg-expand {
cursor: pointer;
float: right;
font-weight: bold;
}
`
: `
.eblaeo-at-user-button {
cursor: pointer;
}
`,
],
]);
};
/**
* Loads the features.
* @returns {Promise<void>}
*/
const loadFeatures = async () => {
if (!isInBlaeo) {
if (eblaeo.user.isAdmin && window.location.pathname.includes('/discussion/9VTBD/')) {
at_addUserButtons(document.body);
DOM.observeNode(document.body, null, /** @type {NodeCallback} */ (at_addUserButtons));
}
return;
}
if (window.location.pathname.includes('/settings')) {
sm_addButton();
} else if (window.location.href.includes('/admin/users/new?steam_id=')) {
at_searchUser();
} else if (window.location.pathname.includes('/posts/new')) {
await pg_addButton();
} else if (window.location.pathname.includes('/posts/')) {
glc_addButton();
}
};
/**
* Shows an alert in a context element.
* @param {HTMLElement} contextEl The context element where to show the alert.
* @param {ExtendedInsertPosition} position Where to insert the alert.
* @param {'loading' | 'success' | 'warning' | 'danger'} alertType The type of the alert.
* @param {string} message The message to show.
*/
const showAlert = (contextEl, position, alertType, message) => {
if (eblaeo.alertEl) {
// prettier-ignore
DOM.insertElements(contextEl, position, [eblaeo.alertEl]);
} else {
// prettier-ignore
DOM.insertElements(contextEl, position, [
['div', {
id: 'eblaeo-alert',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.alertEl = ref),
}, null],
]);
}
if (!eblaeo.alertEl) {
return;
}
eblaeo.alertEl.className = `alert alert-${alertType === 'loading' ? 'info' : alertType}`;
/** @type {ElementArray[]} */
let elArrays;
switch (alertType) {
case 'loading':
// prettier-ignore
elArrays = [
['i', { className: 'fa fa-circle-o-notch fa-spin' }, null],
' ',
message,
];
break;
case 'success':
// prettier-ignore
elArrays = [
['i', { className: 'fa fa-check-circle' }, null],
' ',
message,
];
break;
case 'warning':
// prettier-ignore
elArrays = [
['i', { className: 'fa fa-question-circle' }, null],
' ',
message,
];
break;
case 'danger':
// prettier-ignore
elArrays = [
['i', { className: 'fa fa-times-circle' }, null],
' An error happened: ',
['span', null, message || null],
'. Please try again later. If the error persists, please report it on ',
['a', { href: 'https://gitlab.com/rafaelgssa/monkey-scripts/-/issues' }, 'GitLab'],
'.',
];
break;
// no default
}
// prettier-ignore
DOM.insertElements(eblaeo.alertEl, 'atinner', elArrays);
eblaeo.alertEl.style.display = 'block';
};
/**
* Shows a confirmation dialog.
* @param {string} message The message to show.
* @param {(event: MouseEvent) => unknown | null} [onYes] Callback to call when the 'yes' button is clicked.
* @param {(event: MouseEvent) => unknown | null} [onNo] Callback to call when the 'no' button is clicked.
* @returns {Promise<void>}
*/
const showDialog = async (message, onYes, onNo) => {
if (!eblaeo.dialogModalEl) {
// prettier-ignore
DOM.insertElements(document.body, 'beforeend', [
['button', {
id: 'eblaeo-dialog-modal-button',
dataset: { toggle: 'modal', target: '#eblaeo-dialog-modal' },
ref: (/** @type {HTMLButtonElement} */ ref) => (eblaeo.dialogModalButton = ref),
}, null],
['div', {
id: 'eblaeo-dialog-modal-holder',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalHolderEl = ref),
}, [
['div', {
id: 'eblaeo-dialog-modal',
className: 'modal',
attrs: { role: 'dialog' },
tabIndex: -1,
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalEl = ref),
}, [
['div', { className: 'modal-dialog' }, [
['div', { className: 'modal-content' }, [
['div', { className: 'modal-header' }, [
['button', { type: 'button', dataset: { dismiss: 'modal' } }, [
['i', { className: 'fa fa-close' }, null],
]],
['h4', {
id: 'eblaeo-dialog-modal-label', className: 'modal-title',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalLabelEl = ref),
}, null],
]],
['div', {
className: 'modal-footer',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalFooterEl = ref),
}, [
['button', {
type: 'button',
className: 'btn btn-default',
dataset: { dismiss: 'modal' },
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalDenyButton = ref),
}, 'No'],
['button', {
id: 'eblaeo-pg-dialog-modal-confirm-button',
type: 'button',
className: 'btn btn-primary',
dataset: { dismiss: 'modal' },
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.dialogModalConfirmButton = ref),
}, 'Yes'],
]],
]],
]],
]],
]],
]);
}
if (
!eblaeo.dialogModalButton ||
!eblaeo.dialogModalEl ||
!eblaeo.dialogModalLabelEl ||
!eblaeo.dialogModalFooterEl ||
!eblaeo.dialogModalDenyButton ||
!eblaeo.dialogModalConfirmButton
) {
return;
}
eblaeo.dialogModalLabelEl.textContent = message;
if (onYes || onNo) {
eblaeo.dialogModalFooterEl.style.display = 'block';
if (onYes) {
eblaeo.dialogModalConfirmButton.onclick = onYes;
}
if (onNo) {
eblaeo.dialogModalDenyButton.onclick = onNo;
}
} else {
eblaeo.dialogModalFooterEl.style.display = 'none';
}
eblaeo.dialogModalButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
const isModalOpen = !!(await DOM.dynamicQuerySelector(
'#eblaeo-dialog-modal.modal.in',
60,
0.1
));
if (isModalOpen) {
eblaeo.dialogModalEl.style.zIndex = '1070';
const modalBackdropEl = /** @type {HTMLElement | null} */ (await DOM.dynamicQuerySelector(
'#eblaeo-dialog-modal-holder + .modal-backdrop.in',
60,
0.1
));
if (modalBackdropEl) {
modalBackdropEl.style.zIndex = '1060';
}
}
};
/**
* [SM] Settings Menu
* Allows the user to sync their data.
*/
/**
* Adds a button to the settings page, which allows loading the settings menu.
*/
const sm_addButton = () => {
eblaeo.sm = {};
eblaeo.sm.sidebarNavEl = /** @type {HTMLElement | null} */ (document.querySelector(
'.nav.nav-pills.nav-stacked'
));
if (!eblaeo.sm.sidebarNavEl) {
return;
}
const oldButton = eblaeo.sm.sidebarNavEl.querySelector('#eblaeo-sm-button');
if (oldButton) {
oldButton.remove();
}
// prettier-ignore
[eblaeo.sm.button] = DOM.insertElements(eblaeo.sm.sidebarNavEl, 'beforeend', [
['li', { id: 'eblaeo-sm-button', onclick: sm_loadMenu }, [
['a', { href: '#eblaeo-sm' }, 'Enhanced BLAEO'],
]],
]);
if (window.location.hash === '#eblaeo-sm') {
sm_loadMenu();
}
};
/**
* Loads the settings menu.
*/
const sm_loadMenu = () => {
if (!eblaeo.sm.sidebarNavEl || !eblaeo.sm.button) {
return;
}
eblaeo.sm.mainContainerEl = /** @type {HTMLElement | null} */ (document.querySelector(
'.col-sm-9.col-md-10'
));
if (!eblaeo.sm.mainContainerEl) {
return;
}
window.location.hash = '#eblaeo-sm';
const activeButton = eblaeo.sm.sidebarNavEl.querySelector('.active');
if (activeButton) {
activeButton.classList.remove('active');
}
eblaeo.sm.button.classList.add('active');
// prettier-ignore
DOM.insertElements(eblaeo.sm.mainContainerEl, 'atinner', [
['div', { className: 'panel panel-default' }, [
['div', { className: 'panel-body' }, [
['button', {
type: 'button',
className: 'btn btn-success pull-right',
ref: (/** @type {HTMLButtonElement} */ ref) => (eblaeo.sm.syncButton = ref),
onclick: sm_sync,
}, [
['i', { className: 'fa fa-refresh' }, null],
' Sync now',
]],
['p', null, [
'Your data was last synced ',
['span', {
title: eblaeo.user.lastSync ? Utils.getUtcString(new Date(eblaeo.user.lastSync)) : null,
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.sm.lastSyncEl = ref),
},
eblaeo.user.lastSync ? Utils.getRelativeTimeFromUnix(eblaeo.user.lastSync / 1e3) : 'never'
],
' ago.',
]],
['p', null, [
'Your current username is ',
['b', { ref: (/** @type {HTMLElement} */ ref) => (eblaeo.sm.usernameEl = ref) },
eblaeo.user.username || '?'
],
' and you have ',
['b', { ref: (/** @type {HTMLElement} */ ref) => (eblaeo.sm.ownedGamesEl = ref) },
Object.keys(eblaeo.user.ownedGames).length.toString()
],
' games in your library, right?',
]],
]],
]],
]);
};
/**
* Syncs the user's data.
* @returns {Promise<void>}
*/
const sm_sync = async () => {
if (!eblaeo.sm.mainContainerEl || !eblaeo.sm.syncButton) {
return;
}
if (eblaeo.alertEl) {
eblaeo.alertEl.style.display = 'none';
}
eblaeo.sm.syncButton.disabled = true;
// prettier-ignore
DOM.insertElements(eblaeo.sm.syncButton, 'atinner', [
['i', { className: 'fa fa-refresh fa-spin' }, null],
' Syncing',
]);
try {
await sm_syncUsername(true);
await sm_syncSteamId(true);
await sm_syncAdminStatus(true);
await sm_syncOwnedGames(true);
await sm_finishSync();
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.sm.mainContainerEl, 'beforeend', 'danger', err.message);
} else {
showAlert(eblaeo.sm.mainContainerEl, 'beforeend', 'danger', 'failed to sync data');
}
}
// prettier-ignore
DOM.insertElements(eblaeo.sm.syncButton, 'atinner', [
['i', { className: 'fa fa-refresh' }, null],
' Sync now',
]);
eblaeo.sm.syncButton.disabled = false;
};
/**
* Syncs the user's username.
* @param {boolean} inSettingsMenu Whether the sync will run in the settings menu or not.
* @returns {Promise<void>}
*/
const sm_syncUsername = async (inSettingsMenu) => {
if (!sm_shouldSync(inSettingsMenu)) {
return;
}
try {
const avatarLink = /** @type {HTMLAnchorElement | null} */ (document.querySelector(
'.navbar-btn.btn'
));
if (!avatarLink) {
throw new CustomError('could not retrieve username');
}
const username = avatarLink.href.split('/users/')[1] || '';
if (eblaeo.user.username === username) {
return;
}
eblaeo.user.username = username;
await PersistentStorage.setValue('username', eblaeo.user.username);
if (eblaeo.sm.usernameEl) {
eblaeo.sm.usernameEl.textContent = eblaeo.user.username || '?';
}
} catch (err) {
if (err instanceof CustomError) {
throw err;
}
throw new CustomError('failed to sync username');
}
};
/**
* Syncs the user's Steam ID.
* @param {boolean} inSettingsMenu Whether the sync will run in the settings menu or not.
* @returns {Promise<void>}
*/
const sm_syncSteamId = async (inSettingsMenu) => {
if (eblaeo.user.steamId) {
return;
}
try {
await sm_syncUsername(inSettingsMenu);
if (!eblaeo.user.username) {
throw new CustomError('cannot retrieve Steam ID without username');
}
const user = await BlaeoApi.getUser({ username: eblaeo.user.username });
if (!user) {
throw new CustomError('could not retrieve Steam ID');
}
eblaeo.user.steamId = user.steam_id;
await PersistentStorage.setValue('steamId', eblaeo.user.steamId);
} catch (err) {
if (err instanceof CustomError) {
throw err;
}
throw new CustomError('failed to sync Steam ID');
}
};
/**
* Syncs the user's admin status.
* @param {boolean} inSettingsMenu Whether the sync will run in the settings menu or not.
* @returns {Promise<void>}
*/
const sm_syncAdminStatus = async (inSettingsMenu) => {
if (!sm_shouldSync(inSettingsMenu)) {
return;
}
try {
const isAdmin = !!document.querySelector('[href="/admin"]');
if (eblaeo.user.isAdmin === isAdmin) {
return;
}
eblaeo.user.isAdmin = isAdmin;
await PersistentStorage.setValue('isAdmin', eblaeo.user.isAdmin);
} catch (err) {
if (err instanceof CustomError) {
throw err;
}
throw new CustomError('failed to sync admin status');
}
};
/**
* Syncs the user's owned games.
* @param {boolean} inSettingsMenu Whether the sync will run in the settings menu or not.
* @returns {Promise<void>}
*/
const sm_syncOwnedGames = async (inSettingsMenu) => {
if (!sm_shouldSync(inSettingsMenu)) {
return;
}
try {
await sm_syncSteamId(inSettingsMenu);
if (!eblaeo.user.steamId) {
throw new CustomError('cannot retrieve owned games without Steam ID');
}
eblaeo.user.ownedGames = {};
const games = (await BlaeoApi.getGames({ steamId: eblaeo.user.steamId })) || [];
for (const game of games) {
const { steam_id: id, progress } = game;
eblaeo.user.ownedGames[id] = { id };
if (progress) {
eblaeo.user.ownedGames[id].progress = gameProgresses[progress];
}
}
await PersistentStorage.setValue('ownedGames', eblaeo.user.ownedGames);
if (eblaeo.sm.ownedGamesEl) {
eblaeo.sm.ownedGamesEl.textContent = Object.keys(eblaeo.user.ownedGames).length.toString();
}
if (!inSettingsMenu) {
await sm_finishSync();
}
} catch (err) {
if (err instanceof CustomError) {
throw err;
}
throw new CustomError('failed to sync owned games');
}
};
/**
* Finishes the sync.
* @returns {Promise<void>}
*/
const sm_finishSync = async () => {
try {
eblaeo.user.lastSync = Date.now();
await PersistentStorage.setValue('lastSync', eblaeo.user.lastSync);
if (!eblaeo.sm.lastSyncEl) {
return;
}
eblaeo.sm.lastSyncEl.title = Utils.getUtcString(new Date(eblaeo.user.lastSync));
eblaeo.sm.lastSyncEl.textContent = Utils.getRelativeTimeFromUnix(eblaeo.user.lastSync / 1e3);
} catch (err) {
if (err instanceof CustomError) {
throw err;
}
throw new CustomError('failed to finish sync');
}
};
/**
* Checks if the sync should run.
* @param {boolean} inSettingsMenu Whether the sync will run in the settings menu or not.
* @returns {boolean} Whether the sync should run or not.
*/
const sm_shouldSync = (inSettingsMenu) => {
return inSettingsMenu || Date.now() - eblaeo.user.lastSync > Utils.ONE_WEEK_IN_MILLI;
};
/**
* [AT] Admin Tools
* Allows administrators to easily add new users to the website.
*/
/**
* Adds buttons for users in a context element on SteamGifts, which allow them to be added to BLAEO.
* @param {Element} contextEl The context element where to add the buttons.
*/
const at_addUserButtons = (contextEl) => {
if (!(contextEl instanceof Element)) {
return;
}
const selectors = '.comment__username';
if (contextEl.matches(selectors)) {
at_addUserButton(/** @type {HTMLElement} */ (contextEl));
} else {
const elements = Array.from(
/** @type {NodeListOf<HTMLElement>} */ (contextEl.querySelectorAll(selectors))
);
elements.forEach(at_addUserButton);
}
};
/**
* Adds a button for a user on SteamGifts, which allows them to be added to BLAEO.
* @param {HTMLElement} usernameEl The element containing the user's username.
*/
const at_addUserButton = (usernameEl) => {
const username = (usernameEl.textContent || '').trim();
// prettier-ignore
const [button] = DOM.insertElements(usernameEl, 'beforeend', [
' ',
['img', {
className: 'eblaeo-at-user-button',
src: `${BlaeoApi.BLAEO_URL}/logo-32x32.png`,
alt: 'BLAEO logo',
title: `Add ${username} to BLAEO`,
height: 12,
onclick: () => at_onUserButtonClick(button, username),
}, null],
]);
};
/**
* Triggered when a user button is clicked on SteamGifts.
* @param {HTMLElement | undefined} button The button.
* @param {string} username The user's username.
* @returns {Promise<void>}
*/
const at_onUserButtonClick = async (button, username) => {
try {
await at_openUserTab(username);
if (button) {
button.remove();
}
} catch (err) {
console.log(`[${scriptName}] Failed to open BLAEO admin tab for ${username}:`, err);
window.alert(`Failed to open BLAEO admin tab for ${username}!`);
}
};
/**
* Opens a BLAEO admin tab to search for a user.
* @param {string} username The user's username.
* @returns {Promise<void>}
*/
const at_openUserTab = async (username) => {
const response = await Requests.GET(`https://www.steamgifts.com/user/${username}`);
if (!response.dom) {
throw new Error('Bad request');
}
const steamLink = response.dom.querySelector('[href*="/profiles/"]');
if (!steamLink) {
throw new Error('Could not retrieve Steam ID');
}
const url = steamLink.getAttribute('href');
if (!url) {
throw new Error('Could not retrieve Steam ID');
}
const [, steamId] = url.split('/profiles/');
GM.openInTab(`${BlaeoApi.BLAEO_URL}/admin/users/new?steam_id=${steamId}`, true);
};
/**
* Searches for a user on BLAEO using their Steam ID, so they can be easily added.
*/
const at_searchUser = () => {
const parts = window.location.search.split('steam_id=');
if (parts.length !== 2) {
return;
}
const searchField = /** @type {HTMLInputElement | null} */ (document.querySelector(
'[name="q"]'
));
const searchButton = document.querySelector('.input-group-btn .btn.btn-default');
if (!searchField || !searchButton) {
return;
}
const [, steamId] = parts;
searchField.value = steamId;
searchButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
};
/**
* [GLC] Game List Checker
* Allows the user to keep track of a game list in a post and check which games they own / what their progress is.
*/
/**
* Adds a button to a post if it has a list, which allows checking the list.
*/
const glc_addButton = () => {
eblaeo.glc = {};
eblaeo.glc.postEl = /** @type {HTMLElement | null} */ (document.querySelector(
'.panel-default.post'
));
if (!eblaeo.glc.postEl) {
return;
}
eblaeo.glc.listItemEls = Array.from(eblaeo.glc.postEl.querySelectorAll('li'));
if (eblaeo.glc.listItemEls.length === 0) {
return;
}
// prettier-ignore
DOM.insertElements(eblaeo.glc.postEl, 'afterbegin', [
['div', { id: 'eblaeo-glc-button-container' }, [
['button', {
id: 'eblaeo-glc-button',
type: 'button',
className: 'btn btn-info',
title: 'Check list',
ref: (/** @type {HTMLButtonElement} */ ref) => (eblaeo.glc.button = ref),
onclick: glc_checkList,
}, [
['i', {
className: 'fa fa-search',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.glc.buttonIconEl = ref),
}, null ],
]],
]],
['button', {
id: 'eblaeo-glc-modal-button',
dataset: { toggle: 'modal', target: '#eblaeo-glc-modal' },
ref: (/** @type {HTMLButtonElement} */ ref) => (eblaeo.glc.modalButton = ref),
}, null],
['div', { id: 'eblaeo-glc-modal-holder' }, [
['div', {
id: 'eblaeo-glc-modal',
className: 'modal',
attrs: { role: 'dialog' },
tabIndex: -1,
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.glc.modalEl = ref),
}, [
['div', { className: 'modal-dialog' }, [
['div', { className: 'modal-content' }, [
['div', { className: 'modal-header' }, [
['button', { type: 'button', dataset: { dismiss: 'modal' } }, [
['i', { className: 'fa fa-close' }, null],
]],
['h4', { id: 'eblaeo-glc-modal-label', className: 'modal-title' }, 'List check results'],
]],
['div', {
className: 'modal-body markdown',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.glc.modalBodyEl = ref),
}, null],
]],
]],
]],
]],
]);
};
/**
* Checks a list.
* @returns {Promise<void>}
*/
const glc_checkList = async () => {
if (
!eblaeo.glc.postEl ||
!eblaeo.glc.listItemEls ||
!eblaeo.glc.button ||
!eblaeo.glc.buttonIconEl
) {
return;
}
if (eblaeo.glc.hasChecked) {
return glc_showResults();
}
if (eblaeo.alertEl) {
eblaeo.alertEl.style.display = 'none';
}
eblaeo.glc.button.disabled = true;
eblaeo.glc.button.title = 'Checking list...';
eblaeo.glc.buttonIconEl.className = 'fa fa-circle-o-notch fa-spin';
try {
await sm_syncOwnedGames(false);
eblaeo.glc.isFirstCheck = false;
eblaeo.glc.games = {};
eblaeo.glc.gamesCount = 0;
[, eblaeo.glc.postId] = window.location.pathname.split('/posts/');
if (!eblaeo.user.glcLists[eblaeo.glc.postId]) {
eblaeo.user.glcLists[eblaeo.glc.postId] = [];
eblaeo.glc.isFirstCheck = true;
}
const oldLength = eblaeo.user.glcLists[eblaeo.glc.postId].length;
eblaeo.glc.listItemEls.forEach(glc_checkListItem);
const newLength = eblaeo.user.glcLists[eblaeo.glc.postId].length;
if (newLength > 0 && newLength !== oldLength) {
await PersistentStorage.setValue('glcLists', eblaeo.user.glcLists);
}
eblaeo.glc.hasChecked = true;
eblaeo.glc.button.className = 'btn btn-success';
eblaeo.glc.button.title = 'List checked (click to see results)';
eblaeo.glc.buttonIconEl.className = 'fa fa-check';
eblaeo.glc.button.disabled = false;
return glc_showResults();
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.glc.postEl, 'beforebegin', 'danger', err.message);
} else {
showAlert(eblaeo.glc.postEl, 'beforebegin', 'danger', 'failed to check list');
}
eblaeo.glc.button.title = 'Check list';
eblaeo.glc.buttonIconEl.className = 'fa fa-search';
eblaeo.glc.button.disabled = false;
}
};
/**
* Checks a list item.
* @param {HTMLElement} listItemEl The element of the list item to check.
*/
const glc_checkListItem = (listItemEl) => {
if (!eblaeo.glc.games || !Utils.isSet(eblaeo.glc.gamesCount) || !eblaeo.glc.postId) {
return;
}
const link = /** @type {HTMLAnchorElement | null} */ (listItemEl.querySelector(
'[href*="store.steampowered.com/app/"]'
));
if (!link) {
return;
}
const matches = link.href.match(/\/app\/(\d+)/);
if (!matches) {
return;
}
const id = parseInt(matches[1]);
const name = (listItemEl.textContent || '').trim().replace(/\n*/g, '');
eblaeo.glc.gamesCount += 1;
let isNew = false;
if (!eblaeo.user.glcLists[eblaeo.glc.postId].includes(id)) {
eblaeo.user.glcLists[eblaeo.glc.postId].push(id);
isNew = true;
}
const ownedGame = eblaeo.user.ownedGames[id];
if (ownedGame && ownedGame.progress) {
if (!eblaeo.glc.games[ownedGame.progress]) {
eblaeo.glc.games[ownedGame.progress] = [];
}
// @ts-ignore
eblaeo.glc.games[ownedGame.progress].push({ id, name, isNew });
}
if (!isNew || (ownedGame && ownedGame.progress)) {
return;
}
if (!eblaeo.glc.games.new) {
eblaeo.glc.games.new = [];
}
eblaeo.glc.games.new.push({ id, name });
};
/**
* Shows the results.
*/
const glc_showResults = () => {
if (
!eblaeo.glc.modalButton ||
!eblaeo.glc.modalEl ||
!eblaeo.glc.modalBodyEl ||
!eblaeo.glc.games ||
!Utils.isSet(eblaeo.glc.gamesCount)
) {
return;
}
eblaeo.glc.modalBodyEl.innerHTML = '';
try {
const entries = /** @type {[GameCategoryInfo, GlcGame[]][]} */ (
/** @type {GameCategory[]} */ (gameCategories)
.map((key) =>
(key !== 'new' || !eblaeo.glc.isFirstCheck) && eblaeo.glc.games && eblaeo.glc.games[key]
? [gameCategoryInfos[key], eblaeo.glc.games[key]]
: null
)
.filter(Utils.isSet)
);
for (const [categoryInfo, games] of entries) {
// prettier-ignore
DOM.insertElements(eblaeo.glc.modalBodyEl, 'beforeend', [
['div', {
className: `eblaeo-glc-results-section panel panel-${categoryInfo.bootstrapClass}`,
}, [
['div', { className: 'panel-heading' }, categoryInfo.name],
['div', { className: 'panel-body' }, [
['ul', null,
games.map((game) => /** @type {ElementArray} */ (
['li', null, [
game.isNew && !eblaeo.glc.isFirstCheck ? ['b', null, '[NEW] '] : null,
['a', { href: `https://store.steampowered.com/app/${game.id}` },
game.name || game.id.toString()
],
]]
))
],
]],
]],
]);
}
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.glc.modalBodyEl, 'atinner', 'danger', err.message);
} else {
showAlert(eblaeo.glc.modalBodyEl, 'atinner', 'danger', 'failed to load list check results');
}
}
eblaeo.glc.modalButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
const counterEl = document.querySelector('[id*="counter"]');
if (!counterEl) {
return;
}
// prettier-ignore
DOM.insertElements(counterEl, 'atinner', [
['font', { size: '4' }, [
['b', null, `${eblaeo.glc.gamesCount} Games`],
]],
]);
};
/**
* [PG] Post Generator
* Allows the user to easily generate posts.
*/
/**
* Adds a button to the new post page, which allows generating posts.
* @returns {Promise<void> | void}
*/
const pg_addButton = () => {
eblaeo.pg = {};
eblaeo.pg.postField = /** @type {HTMLTextAreaElement | null} */ (document.querySelector(
'[name="post[text]"]'
));
eblaeo.pg.previewButton = /** @type {HTMLElement | null} */ (document.querySelector(
'#get-preview'
));
eblaeo.pg.createButton = /** @type {HTMLElement | null} */ (document.querySelector(
'.btn.btn-primary.pull-right'
));
if (!eblaeo.pg.postField || !eblaeo.pg.previewButton || !eblaeo.pg.createButton) {
return;
}
pg_loadInitialValues();
eblaeo.pg.createButton.addEventListener('click', pg_deleteCaches);
// prettier-ignore
DOM.insertElements(eblaeo.pg.createButton, 'afterend', [
['button', {
id: 'eblaeo-pg-generate-button',
type: 'button',
className: 'btn btn-default pull-right',
dataset: { toggle: 'modal', target: '#eblaeo-pg-modal' },
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.generateButton = ref),
onclick: pg_focusSearchGamesField,
}, 'Generate'],
]);
// prettier-ignore
DOM.insertElements(document.body, 'beforeend', [
['div', { id: 'eblaeo-pg-modal-holder' }, [
['div', {
id: 'eblaeo-pg-modal',
className: 'modal',
attrs: { role: 'dialog' },
tabIndex: -1,
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.modalEl = ref),
}, [
['div', { className: 'modal-dialog modal-lg' }, [
['div', { className: 'modal-content' }, [
['div', { className: 'modal-header' }, [
['button', { type: 'button', dataset: { dismiss: 'modal' } }, [
['i', { className: 'fa fa-close' }, null],
]],
['h4', { id: 'eblaeo-pg-modal-label', className: 'modal-title' }, 'Generate post'],
]],
['div', {
className: 'modal-body',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.modalBodyEl = ref),
}, [
['input', {
id: 'eblaeo-pg-search-games',
type: 'text',
className: 'form-control',
placeholder: 'Start typing to search for games …',
dataset: { target: '#eblaeo-pg-search-games-results' },
ref: (/** @type {HTMLInputElement} */ ref) => (eblaeo.pg.searchGamesField = ref),
oninput: pg_searchGames,
}, null],
['br', null, null],
['div', {
id: 'eblaeo-pg-search-games-results',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.searchGamesResultsEl = ref),
}, null],
['br', null, null],
['div', {
id: 'eblaeo-pg-generator',
className: 'panel panel-default',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.generatorEl = ref),
}, [
['div', { className: 'panel-heading' }, 'Generator'],
['div', {
id: 'eblaeo-pg-generator-body',
className: 'panel-body',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.generatorBodyEl = ref),
}, pg_getGeneratorBody()],
]],
['div', {
id: 'eblaeo-pg-game-preview-container',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.gamePreviewContainerEl = ref),
}, [
['div', {
id: 'eblaeo-pg-game-preview',
className: 'panel panel-default',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.gamePreviewEl = ref),
}, [
['div', {
className: 'panel-heading',
dataset: { toggle: 'collapse', target: '#eblaeo-pg-game-preview-body' },
}, [
'Game preview',
['span', { className: 'eblaeo-pg-collapse' }, [
'Collapse ',
['i', { className: 'fa fa-level-up' }],
]],
['span', { className: 'eblaeo-pg-expand' }, [
'Expand ',
['i', { className: 'fa fa-level-down' }],
]],
]],
['div', {
id: 'eblaeo-pg-game-preview-body',
className: 'panel-body collapse in',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.gamePreviewBodyEl = ref),
}, null],
['div', { className: 'panel-footer' }, [
['button', {
id: 'eblaeo-pg-game-preview-button',
type: 'button',
className: 'btn btn-primary pull-right',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.gamePreviewButton = ref),
}, null],
['div', { className: 'clear-both' }, null],
]],
]],
]],
['div', {
id: 'eblaeo-pg-full-preview',
className: 'panel panel-default',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.fullPreviewEl = ref),
}, [
['div', {
className: 'panel-heading',
dataset: { toggle: 'collapse', target: '#eblaeo-pg-full-preview-body' },
}, [
'Full preview',
['span', { className: 'eblaeo-pg-collapse' }, [
'Collapse ',
['i', { className: 'fa fa-level-up' }],
]],
['span', { className: 'eblaeo-pg-expand' }, [
'Expand ',
['i', { className: 'fa fa-level-down' }],
]],
]],
['div', {
id: 'eblaeo-pg-full-preview-body',
className: 'panel-body collapse in',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.fullPreviewBodyEl = ref),
}, null],
]],
]],
['div', { className: 'modal-footer' }, [
['button', {
type: 'button',
className: 'btn btn-default',
dataset: { dismiss: 'modal' },
}, 'Cancel'],
['button', {
id: 'eblaeo-pg-done-button',
type: 'button',
className: 'btn btn-primary',
dataset: { dismiss: 'modal' },
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.doneButton = ref),
onclick: pg_generatePost,
}, 'Done'],
]],
]],
]],
]],
]],
]);
return pg_loadCaches();
};
/**
* Loads the initial values.
*/
const pg_loadInitialValues = () => {
const defaultPresetBase = /** @type {PgPresetBase} */ ({
showPlaytimeThisMonth: false,
playtimeTemplate: '%playtime% playtime',
linkAchievements: true,
achievementsTemplate: '%achievements_unlocked% of %achievements_total% achievements',
noAchievementsTemplate: 'no achievements',
checkScreenshots: false,
linkScreenshots: true,
screenshotsTemplate: '%screenshots_count% screenshots',
noScreenshotsTemplate: 'no screenshots',
bgType: 'Solid',
bgColor1: '#ffffff',
bgColor2: '#000000',
titleColor: '#555555',
textColor: '#333333',
linkColor: '#337ab7',
});
eblaeo.pg.defaultPresets = {
'Box default': {
type: 'box',
name: 'Box default',
prefs: {
...defaultPresetBase,
reviewPosition: 'Left',
},
},
'Bar default': {
type: 'bar',
name: 'Bar default',
prefs: {
showInfoInOneLine: false,
...defaultPresetBase,
achievementsTemplate:
'%achievements_unlocked% of %achievements_total% achievements (%achievements_percentage%%)',
titleColor: '#333333',
completionBarPosition: 'Left',
imagePosition: 'Left',
useCollapsibleReview: false,
reviewTriggerMethod: 'Bar click',
},
},
'Panel default': {
type: 'panel',
name: 'Panel default',
prefs: {
...defaultPresetBase,
achievementsTemplate:
'%achievements_unlocked% of %achievements_total% achievements (%achievements_percentage%%)',
usePredefinedTheme: true,
predefinedThemeColor: 'Blue',
useCustomTheme: false,
useCollapsibleReview: false,
},
},
'Custom default': {
type: 'custom',
name: 'Custom default',
prefs: {
htmlTemplate: '',
},
},
};
eblaeo.pg.defaultGame = {
id: 0,
name: '',
image: '',
progress: 'uncategorized',
playtime: {
thisMonth: 0,
total: 0,
},
achievements: {
unlocked: 0,
total: 0,
},
screenshotsCount: 0,
customHtml: '',
rating: '',
review: '',
preset: {
...eblaeo.pg.defaultPresets['Box default'],
prefs: {
...eblaeo.pg.defaultPresets['Box default'].prefs,
},
},
};
eblaeo.pg.presetTypes = ['box', 'bar', 'panel', 'custom'];
eblaeo.pg.presetTypeNames = {
box: 'Box',
bar: 'Bar',
panel: 'Panel',
custom: 'Custom',
};
eblaeo.pg.presets = {
box: {
...eblaeo.pg.defaultPresets['Box default'],
prefs: {
...eblaeo.pg.defaultPresets['Box default'].prefs,
},
},
bar: {
...eblaeo.pg.defaultPresets['Bar default'],
prefs: {
...eblaeo.pg.defaultPresets['Bar default'].prefs,
},
},
panel: {
...eblaeo.pg.defaultPresets['Panel default'],
prefs: {
...eblaeo.pg.defaultPresets['Panel default'].prefs,
},
},
custom: {
...eblaeo.pg.defaultPresets['Custom default'],
prefs: {
...eblaeo.pg.defaultPresets['Custom default'].prefs,
},
},
};
eblaeo.pg.currentPresetType = 'box';
eblaeo.pg.gameInfos = [];
eblaeo.pg.selectedGame = null;
eblaeo.pg.isEditing = false;
eblaeo.pg.presetTabNavEls = {};
eblaeo.pg.presetTabEls = {};
eblaeo.pg.presetDropdownEls = {};
eblaeo.pg.fieldContainerEls = {
presets: {
box: {},
bar: {},
panel: {},
custom: {},
},
};
eblaeo.pg.fields = {
presets: {
box: {},
bar: {},
panel: {},
custom: {},
},
};
eblaeo.pg.isSearchingGames = false;
eblaeo.pg.hasNewGameSearchQuery = false;
};
/**
* Returns element arrays for the generator body.
* @returns {ElementArray[]} The element arrays for the body.
*/
const pg_getGeneratorBody = () => {
if (!eblaeo.pg.presetTypeNames) {
return [];
}
// prettier-ignore
return /** @type {ElementArray[]} */ ([
['div', null, [
['p', null, 'These placeholders are replaced with info about you:'],
['ul', null, [
['li', null, [['b', null, '%steamid%'], ' - Your Steam ID.']],
['li', null, [['b', null, '%username%'], ' - Your BLAEO username (this can be your SteamGifts or Steam username depending on your BLAEO settings).']],
]],
['p', null, 'These placeholders are replaced with info about the game:'],
['ul', null, [
['li', null, [['b', null, '%id%'], ' - The Steam ID of the game.']],
['li', null, [['b', null, '%name%'], ' - The name of the game.']],
['li', null, [['b', null, '%image%'], ' - The URL of the game image.']],
['li', null, [['b', null, '%progress%'], " - The progress of the game ('uncategorized', 'never-played', 'unfinished', 'beaten', 'completed' or 'wont-play')"]],
['li', null, [['b', null, '%progress_name%'], " - The name for the progress of the game ('Uncategorized', 'Never Played', 'Unfinished', 'Beaten', 'Completed' or \"Won't Play\")"]],
['li', null, [['b', null, '%progress_color%'], ' - The HEX color for the progress of the game.']],
['li', null, [['b', null, '%playtime%'], " - Your playtime in the '12 hours' format."]],
['li', null, [['b', null, '%playtime_this_month%'], " - Your playtime this month in the '12 hours' format."]],
['li', null, [['b', null, '%achievements%'], " - Your achievements in the 'X of Y achievements' or 'no achievements' format."]],
['li', null, [['b', null, '%achievements_unlocked%'], ' - The number of achievements you have unlocked in the game.']],
['li', null, [['b', null, '%achievements_total%'], ' - The total number of achievements in the game.']],
['li', null, [['b', null, '%achievements_percentage%'], ' - The percentage of achievements you have unlocked in the game.']],
['li', null, [['b', null, '%screenshots%'], " - Your screenshots in the 'X screenshots' or 'no screenshots' format (if the option to check screenshots is enabled)."]],
['li', null, [['b', null, '%screenshots_count%'], ' - The number of screenshots you have taken for the game (if the option to check screenshots is enabled)']],
]],
['br', null, null],
['ul', {
id: 'eblaeo-pg-generator-nav',
className: 'nav nav-tabs',
attrs: { role: 'tablist' },
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.generatorNavEl = ref),
},
/** @type [PgPresetType, string][] */ (Object.entries(eblaeo.pg.presetTypeNames)).map(
pg_getPresetTabNav
)
],
['div', { className: 'tab-content' }, [
pg_getBoxTab(),
pg_getBarTab(),
pg_getPanelTab(),
pg_getCustomTab(),
]],
['div', { className: 'form-group' }, [
pg_getField(null, {
type: 'textarea',
id: 'review',
htmlId: 'review',
label: 'Review',
usePlaceholders: true,
}),
]],
['div', { className: 'form-group' }, [
pg_getField(null, {
type: 'text',
id: 'presetName',
htmlId: 'preset-name',
label: 'Preset name',
description: 'Save these preferences as a preset to quickly reuse later.',
}),
]],
['button', {
id: 'eblaeo-pg-save-preset-button',
type: 'button',
className: 'btn btn-default',
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.savePresetButton = ref),
onclick: pg_savePreset,
}, 'Save preset'],
['br', null, null],
['br', null, null],
]],
]);
};
/**
* Returns an element array for a preset tab nav.
* @param {[PgPresetType, string]} presetTypeNameEntry The preset type and name for the tab nav.
* @returns {ElementArray} The element array for the tab nav.
*/
const pg_getPresetTabNav = ([presetType, presetName]) => {
if (!eblaeo.pg.presetTabNavEls) {
return null;
}
const tabId = `eblaeo-pg-tab-${presetType}`;
// prettier-ignore
return /** @type {ElementArray} */ (
['li', {
attrs: { role: 'presentation' },
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => eblaeo.pg.presetTabNavEls[presetType] = ref,
}, [
['a', {
href: `#${tabId}`,
attrs: { role: 'tab' },
dataset: { toggle: 'tab' },
onclick: () => pg_changeCurrentPreset(presetType),
}, presetName],
]]
);
};
/**
* Returns an element array for a box tab.
* @returns {ElementArray} The element array for the tab.
*/
const pg_getBoxTab = () => {
if (!eblaeo.pg.presetTabEls) {
return null;
}
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
id: 'eblaeo-pg-tab-box',
className: 'tab-pane',
attrs: { role: 'tabpanel' },
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.presetTabEls.box = ref),
}, [
['div', { className: 'form-group' }, [
pg_getPresetDropdown('box'),
['br', null, null],
...pg_getPresetBaseFields('box'),
pg_getField('box', {
type: 'select',
id: 'reviewPosition',
htmlId: 'review-position',
label: 'Review position',
selectOptions: ['Left', 'Right'],
}),
]],
]]
);
};
/**
* Returns an element array for a bar tab.
* @returns {ElementArray} The element array for the tab.
*/
const pg_getBarTab = () => {
if (!eblaeo.pg.presetTabEls) {
return null;
}
const presetBaseFields = pg_getPresetBaseFields('bar');
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
id: 'eblaeo-pg-tab-bar',
className: 'tab-pane',
attrs: { role: 'tabpanel' },
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.presetTabEls.bar = ref),
}, [
['div', { className: 'form-group' }, [
pg_getPresetDropdown('bar'),
['br', null, null],
pg_getField('bar', {
type: 'checkbox',
id: 'showInfoInOneLine',
htmlId: 'show-info-in-one-line',
label: 'Show playtime, achievements and screenshots in one line.',
}),
...presetBaseFields.slice(0, 9),
pg_getField(null, {
type: 'text',
id: 'customHtml',
htmlId: 'custom-html',
label: 'Custom HTML',
usePlaceholders: true,
description: 'Forces playtime, achievements and screenshots to be shown in one line, and shows the custom HTML below the line.'
}),
['div', { className: 'eblaeo-pg-double-field-container' }, [
pg_getField('bar', {
type: 'select',
id: 'completionBarPosition',
htmlId: 'completion-bar-position',
label: 'Completion bar position',
selectOptions: ['Left', 'Right', 'Hidden'],
}),
pg_getField('bar', {
type: 'select',
id: 'imagePosition',
htmlId: 'image-position',
label: 'Image position',
selectOptions: ['Left', 'Right'],
}),
]],
...presetBaseFields.slice(9),
pg_getField('bar', {
type: 'checkbox',
id: 'useCollapsibleReview',
htmlId: 'use-collapsible-review',
label: 'Use collapsible review.',
}),
pg_getField('bar', {
type: 'select',
id: 'reviewTriggerMethod',
htmlId: 'review-trigger-method',
label: 'Review trigger method',
selectOptions: ['Bar click', 'Button click'],
}),
]],
]]
);
};
/**
* Returns an element array for a panel tab.
* @returns {ElementArray} The element array for the tab.
*/
const pg_getPanelTab = () => {
if (!eblaeo.pg.presetTabEls) {
return null;
}
const presetBaseFields = pg_getPresetBaseFields('panel');
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
id: 'eblaeo-pg-tab-panel',
className: 'tab-pane',
attrs: { role: 'tabpanel' },
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.presetTabEls.panel = ref),
}, [
['div', { className: 'form-group' }, [
pg_getPresetDropdown('panel'),
['br', null, null],
pg_getField(null, {
type: 'text',
id: 'rating',
htmlId: 'rating',
label: 'Rating',
}),
...presetBaseFields.slice(0, 9),
pg_getField('panel', {
type: 'radio',
id: 'usePredefinedTheme',
htmlId: 'use-predefined-theme',
label: 'Use predefined theme.',
}),
pg_getField('panel', {
type: 'select',
id: 'predefinedThemeColor',
htmlId: 'predefined-theme-color',
label: 'Predefined theme color',
selectOptions: ['Blue', 'Green', 'Grey', 'Red', 'Yellow'],
}),
pg_getField('panel', {
type: 'radio',
id: 'useCustomTheme',
htmlId: 'use-custom-theme',
label: 'Use custom theme.',
}),
...presetBaseFields.slice(9),
pg_getField('panel', {
type: 'checkbox',
id: 'useCollapsibleReview',
htmlId: 'use-collapsible-review',
label: 'Use collapsible review.',
}),
]],
]]
);
};
/**
* Returns an element array for a custom tab.
* @returns {ElementArray} The element array for the tab.
*/
const pg_getCustomTab = () => {
if (!eblaeo.pg.presetTabEls) {
return null;
}
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
id: 'eblaeo-pg-tab-custom',
className: 'tab-pane',
attrs: { role: 'tabpanel' },
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.presetTabEls.custom = ref),
}, [
['div', { className: 'form-group' }, [
pg_getPresetDropdown('custom'),
['br', null, null],
pg_getField('custom', {
type: 'textarea',
id: 'htmlTemplate',
htmlId: 'html-template',
label: 'Custom HTML template',
usePlaceholders: true,
useReviewPlaceholder: true,
}),
]],
]]
);
};
/**
* Returns an element array for a preset dropdown.
* @param {PgPresetType} presetType The preset type for the dropdown.
* @returns {ElementArray} The element array for the dropdown.
*/
const pg_getPresetDropdown = (presetType) => {
if (!eblaeo.pg.presetDropdownEls) {
return null;
}
const buttonId = `eblaeo-pg-apply-preset-button-${presetType}`;
const presets = Object.values(eblaeo.user.pgPresets).filter(
(preset) => preset.type === presetType
);
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
className: 'dropdown',
// @ts-expect-error
ref: (/** @type {HTMLElement} */ ref) => (eblaeo.pg.presetDropdownEls[presetType] = ref),
}, [
['button', { id: buttonId, type: 'button', dataset: { toggle: 'dropdown' } }, [
`Apply ${presetType} preset`,
['span', { className: 'caret' }, null],
]],
['ul', { id: `eblaeo-pg-preset-dropdown-${presetType}`, className: 'dropdown-menu' },
presets.length > 0 ? presets.map(pg_getPresetDropdownListItem) : null
],
]]
);
};
/**
* Returns an element array for a preset dropdown list item.
* @param {PgPreset<PgPresetType>} preset The preset for the list item.
* @returns {ElementArray} The element array for the list item.
*/
const pg_getPresetDropdownListItem = (preset) => {
/** @type {HTMLElement} */
let listItemEl;
// prettier-ignore
return /** @type {ElementArray} */ (
['li', { ref: (/** @type {HTMLElement} */ ref) => (listItemEl = ref) }, [
['a', { onclick: () => pg_applyPreset(preset.type, preset.name) }, preset.name],
['i', {
className: 'fa fa-trash',
title: 'Delete preset',
onclick: () => pg_deletePreset(preset.type, preset.name, listItemEl),
}, null],
]]
);
};
/**
* Returns element arrays for preset base fields.
* @param {PgPresetType} presetType The preset type for the fields.
* @returns {ElementArray[]} The element arrays for the fields.
*/
const pg_getPresetBaseFields = (presetType) => {
// prettier-ignore
return /** @type {ElementArray[]} */ ([
pg_getField(presetType, {
type: 'checkbox',
id: 'showPlaytimeThisMonth',
htmlId: 'show-playtime-this-month',
label: 'Show your playtime for the game this month.',
}),
pg_getField(presetType, {
type: 'text',
id: 'playtimeTemplate',
htmlId: 'playtime-template',
label: 'Playtime template',
usePlaceholders: true,
}),
pg_getField(presetType, {
type: 'checkbox',
id: 'linkAchievements',
htmlId: 'link-achievements',
label: 'Link achievements to your achievements page for the game.',
}),
pg_getField(presetType, {
type: 'text',
id: 'achievementsTemplate',
htmlId: 'achievements-template',
label: 'Achievements template',
usePlaceholders: true,
}),
pg_getField(presetType, {
type: 'text',
id: 'noAchievementsTemplate',
htmlId: 'no-achievements-template',
label: 'No achievements template',
usePlaceholders: true,
}),
pg_getField(presetType, {
type: 'checkbox',
id: 'checkScreenshots',
htmlId: 'check-screenshots',
label: 'Check if you have screenshots for the game.',
}),
pg_getField(presetType, {
type: 'checkbox',
id: 'linkScreenshots',
htmlId: 'link-screenshots',
label: 'Link screenshots to your screenshots page for the game.',
}),
pg_getField(presetType, {
type: 'text',
id: 'screenshotsTemplate',
htmlId: 'screenshots-template',
label: 'Screenshots template',
usePlaceholders: true,
}),
pg_getField(presetType, {
type: 'text',
id: 'noScreenshotsTemplate',
htmlId: 'no-screenshots-template',
label: 'No screenshots template',
usePlaceholders: true,
}),
pg_getField(presetType, {
type: 'select',
id: 'bgType',
htmlId: 'bg-type',
label: 'Background type',
selectOptions: ['Solid', 'Horizontal gradient', 'Vertical gradient'],
}),
['div', { className: 'eblaeo-pg-double-field-container' }, [
pg_getField(presetType, {
type: 'color',
id: 'bgColor1',
htmlId: 'bg-color-1',
label: 'Background color 1',
}),
pg_getField(presetType, {
type: 'color',
id: 'bgColor2',
htmlId: 'bg-color-2',
label: 'Background color 2',
}),
]],
['div', { className: 'eblaeo-pg-triple-field-container' }, [
pg_getField(presetType, {
type: 'color',
id: 'titleColor',
htmlId: 'title-color',
label: 'Title color',
}),
pg_getField(presetType, {
type: 'color',
id: 'textColor',
htmlId: 'text-color',
label: 'Text color',
}),
pg_getField(presetType, {
type: 'color',
id: 'linkColor',
htmlId: 'link-color',
label: 'Link color',
}),
]],
]);
};
/**
* Returns element arrays for a field.
* @param {PgPresetType | null} presetType The preset type for the field, if any.
* @param {PgFieldOptions} options The options for the field.
* @returns {ElementArray[]} The element arrays for the field.
*/
const pg_getField = (presetType, options) => {
let elArrays = /** @type {ElementArray[]} */ ([]);
const fieldId = `eblaeo-pg-${presetType ? `${presetType}-` : ''}${options.htmlId}`;
switch (options.type) {
case 'textarea':
case 'text':
case 'color':
// prettier-ignore
elArrays.push(['label', { htmlFor: fieldId }, `${options.label}:`]);
if (options.usePlaceholders) {
// prettier-ignore
elArrays.push(
['p', null, `You can use placeholders here. ${
options.useReviewPlaceholder
? 'Additionally, place `<div id="review-%username%-%id%"></div>` (without the `) where you want the review to appear.'
: ''
} ${options.description || ''}`]
);
} else if (options.description) {
// prettier-ignore
elArrays.push(['p', null, options.description]);
}
if (options.type === 'textarea') {
// prettier-ignore
elArrays.push(
['textarea', {
id: fieldId,
className: 'form-control',
rows: 5,
ref: (/** @type {HTMLElement} */ ref) => pg_assignField(ref, presetType, options),
onchange: options.id === 'review' ? pg_gamePreview : null,
oninput: options.id === 'review' ? null : pg_gamePreview,
}, null]
);
} else {
// prettier-ignore
elArrays.push(
['input', {
id: fieldId,
type: options.type,
className: 'form-control',
ref: (/** @type {HTMLElement} */ ref) => pg_assignField(ref, presetType, options),
oninput: pg_gamePreview,
}, null]
);
}
break;
case 'checkbox':
// prettier-ignore
elArrays.push(
['div', { className: 'checkbox' }, [
['label', { htmlFor: fieldId }, [
['input', {
id: fieldId,
type: 'checkbox',
ref: (/** @type {HTMLElement} */ ref) => pg_assignField(ref, presetType, options),
onchange: pg_gamePreview,
}, null],
options.label,
]],
]]
);
break;
case 'radio':
// prettier-ignore
elArrays.push(
['div', { className: 'radio' }, [
['label', { htmlFor: fieldId }, [
['input', {
id: fieldId,
type: 'radio',
name: 'optradio',
ref: (/** @type {HTMLElement} */ ref) => pg_assignField(ref, presetType, options),
onchange: pg_gamePreview,
}, null],
options.label,
]],
]]
);
break;
case 'select':
// prettier-ignore
elArrays.push(
['label', { htmlFor: fieldId }, `${options.label}:`],
['select', {
id: fieldId,
className: 'form-control',
ref: (/** @type {HTMLElement} */ ref) => pg_assignField(ref, presetType, options),
onchange: pg_gamePreview,
},
options.selectOptions.map((option) => /** @type {ElementArray} */ (
['option', null, option]
))
]
);
break;
}
// prettier-ignore
elArrays = ['div', {
className: 'eblaeo-pg-field-container',
ref: (/** @type {HTMLElement} */ ref) => pg_assignFieldContainer(ref, presetType, options),
}, elArrays];
return elArrays;
};
/**
* Assigns a field container to a variable.
* @param {HTMLElement} field The field container to assign.
* @param {PgPresetType | null} presetType The preset type for the field container, if any.
* @param {PgFieldOptions} options The options for the field container.
*/
const pg_assignFieldContainer = (field, presetType, options) => {
if (!eblaeo.pg.fieldContainerEls) {
return;
}
if (presetType) {
// @ts-expect-error
eblaeo.pg.fieldContainerEls.presets[presetType][options.id] = field;
} else {
// @ts-expect-error
eblaeo.pg.fieldContainerEls[options.id] = field;
}
};
/**
* Assigns a field to a variable.
* @param {HTMLElement} field The field to assign.
* @param {PgPresetType | null} presetType The preset type for the field, if any.
* @param {PgFieldOptions} options The options for the field.
*/
const pg_assignField = (field, presetType, options) => {
if (!eblaeo.pg.fields) {
return;
}
if (presetType) {
// @ts-expect-error
eblaeo.pg.fields.presets[presetType][options.id] = field;
} else {
// @ts-expect-error
eblaeo.pg.fields[options.id] = field;
}
};
/**
* Changes the current preset.
* @param {PgPresetType} presetType The type of the new preset.
*/
const pg_changeCurrentPreset = (presetType) => {
eblaeo.pg.currentPresetType = presetType;
if (!eblaeo.pg.presets || !eblaeo.pg.selectedGame) {
return;
}
eblaeo.pg.selectedGame.preset = eblaeo.pg.presets[presetType];
pg_selectGame(eblaeo.pg.selectedGame, eblaeo.pg.isEditing || false);
};
/**
* Applies a preset.
* @param {PgPresetType} presetType The type of the preset to apply.
* @param {string} presetName The name of the preset to apply.
*/
const pg_applyPreset = (presetType, presetName) => {
if (
!eblaeo.pg.defaultPresets ||
!eblaeo.pg.presetTypeNames ||
!eblaeo.pg.presets ||
!eblaeo.pg.selectedGame ||
!eblaeo.pg.fields ||
!eblaeo.pg.fields.presetName
) {
return;
}
const preset =
eblaeo.user.pgPresets[presetName] ||
eblaeo.pg.defaultPresets[presetName] ||
eblaeo.pg.defaultPresets[`${eblaeo.pg.presetTypeNames[presetType]}} default`];
eblaeo.pg.presets[presetType] = {
...preset,
prefs: {
...preset.prefs,
},
};
eblaeo.pg.currentPresetType = presetType;
eblaeo.pg.selectedGame.preset = eblaeo.pg.presets[presetType];
eblaeo.pg.fields.presetName.value = eblaeo.pg.presets[presetType].name;
pg_selectGame(eblaeo.pg.selectedGame, eblaeo.pg.isEditing || false);
};
/**
* Deletes a preset.
* @param {PgPresetType} presetType The type of the preset to delete.
* @param {string} presetName The name of the preset to delete.
* @param {HTMLElement} [listItemEl] The element of the list item for the preset, if any.
*/
const pg_deletePreset = (presetType, presetName, listItemEl) => {
showDialog('Are you sure you want to delete this preset?', async () => {
const contextEl = eblaeo.pg.presetDropdownEls && eblaeo.pg.presetDropdownEls[presetType];
if (!contextEl) {
return;
}
try {
delete eblaeo.user.pgPresets[presetName];
await PersistentStorage.setValue('pgPresets', eblaeo.user.pgPresets);
if (listItemEl) {
listItemEl.remove();
}
showAlert(contextEl, 'afterend', 'success', 'Preset deleted!');
} catch (err) {
if (err instanceof CustomError) {
showAlert(contextEl, 'afterend', 'danger', err.message);
} else {
showAlert(contextEl, 'afterend', 'danger', 'failed to delete preset');
}
}
});
};
/**
* Saves the current preset.
* @returns Promise<void>
*/
const pg_savePreset = async () => {
if (
!eblaeo.pg.presets ||
!eblaeo.pg.currentPresetType ||
!eblaeo.pg.presetDropdownEls ||
!eblaeo.pg.fields ||
!eblaeo.pg.fields.presetName ||
!eblaeo.pg.savePresetButton
) {
return;
}
try {
eblaeo.pg.savePresetButton.textContent = 'Saving...';
const presetName =
eblaeo.pg.fields.presetName.value ||
`Untitled preset ${Object.keys(eblaeo.user.pgPresets).length + 1}`;
const currentPreset = eblaeo.pg.presets[eblaeo.pg.currentPresetType];
eblaeo.user.pgPresets[presetName] = {
...currentPreset,
name: presetName,
prefs: {
...currentPreset.prefs,
},
};
await PersistentStorage.setValue('pgPresets', eblaeo.user.pgPresets);
const dropdownEl = eblaeo.pg.presetDropdownEls[eblaeo.pg.currentPresetType];
if (dropdownEl) {
const dropdownListEl = dropdownEl.lastElementChild;
if (dropdownListEl) {
// prettier-ignore
DOM.insertElements(dropdownListEl, 'beforeend', [
pg_getPresetDropdownListItem(eblaeo.user.pgPresets[presetName])
]);
}
}
showAlert(eblaeo.pg.savePresetButton, 'afterend', 'success', 'Preset saved!');
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.savePresetButton, 'afterend', 'danger', err.message);
} else {
showAlert(eblaeo.pg.savePresetButton, 'afterend', 'danger', 'failed to save preset');
}
}
eblaeo.pg.savePresetButton.textContent = 'Save preset';
};
/**
* Gives focus to the search games field when the modal is open.
* @returns {Promise<void>}
*/
const pg_focusSearchGamesField = async () => {
if (!eblaeo.pg.searchGamesField) {
return;
}
const isModalOpen = !!(await DOM.dynamicQuerySelector('#eblaeo-pg-modal.modal.in', 60, 0.1));
if (isModalOpen) {
eblaeo.pg.searchGamesField.focus();
}
};
/**
* Searches for games.
* @returns {Promise<void>}
*/
const pg_searchGames = async () => {
if (!eblaeo.pg.searchGamesField || !eblaeo.pg.searchGamesResultsEl) {
return;
}
if (eblaeo.pg.isSearchingGames) {
eblaeo.pg.hasNewGameSearchQuery = true;
return;
}
try {
eblaeo.pg.isSearchingGames = true;
eblaeo.pg.searchGamesResultsEl.innerHTML = '';
await sm_syncSteamId(false);
const query = eblaeo.pg.searchGamesField.value;
if (query) {
const listEl = await BlaeoApi.searchGames({ steamId: eblaeo.user.steamId }, query);
if (listEl) {
eblaeo.pg.searchGamesResultsEl.appendChild(listEl);
const elements = Array.from(
/** @type {NodeListOf<HTMLElement>} */ (listEl.querySelectorAll('.game'))
);
elements.forEach(pg_addGameListItemButton);
}
}
eblaeo.pg.isSearchingGames = false;
if (!eblaeo.pg.hasNewGameSearchQuery) {
return;
}
eblaeo.pg.hasNewGameSearchQuery = false;
pg_searchGames();
} catch (err) {
eblaeo.pg.isSearchingGames = false;
if (err instanceof CustomError) {
showAlert(eblaeo.pg.searchGamesResultsEl, 'afterend', 'danger', err.message);
} else {
showAlert(
eblaeo.pg.searchGamesResultsEl,
'afterend',
'danger',
'failed to search for games'
);
}
}
};
/**
* Adds a button to a game list item, which allows selecting it.
* @param {HTMLElement} listItemEl The element of the game list item where to add the button.
*/
const pg_addGameListItemButton = (listItemEl) => {
// prettier-ignore
DOM.insertElements(listItemEl, 'beforeend', [
['button', {
type: 'button',
className: 'eblaeo-pg-game-list-item-button btn btn-default',
onclick: () => pg_selectGameListItem(listItemEl),
}, 'Select'],
]);
};
/**
* Selects a game list item.
* @param {HTMLElement} listItemEl The element of the game list item to select.
* @returns {Promise<void>}
*/
const pg_selectGameListItem = async (listItemEl) => {
if (!eblaeo.pg.presets || !eblaeo.pg.currentPresetType || !eblaeo.pg.searchGamesResultsEl) {
return;
}
try {
const link = /** @type {HTMLAnchorElement | null} */ (listItemEl.querySelector('a'));
if (!link) {
return;
}
const url = link.href;
if (!url) {
return;
}
const matches = url.match(/\/app\/(\d+)/);
if (!matches) {
return;
}
const gameId = parseInt(matches[1]);
const game = await BlaeoApi.getGame({ steamId: eblaeo.user.steamId }, gameId);
if (!game) {
throw new CustomError('could not retrieve game');
}
const imageEl = listItemEl.querySelector('img');
const currentPreset = eblaeo.pg.presets[eblaeo.pg.currentPresetType];
eblaeo.pg.presets[eblaeo.pg.currentPresetType] = {
...currentPreset,
prefs: {
...currentPreset.prefs,
},
};
const pgGame = /** @type {PgGame} */ ({
id: gameId,
name: game.name,
image: (imageEl && imageEl.src) || '',
progress: game.progress ? gameProgresses[game.progress] : 'uncategorized',
playtime: {
thisMonth: 0,
total: game.playtime,
},
achievements: game.achievements || {
unlocked: 0,
total: 0,
},
screenshotsCount: 0,
customHtml: '',
rating: '',
review: '',
preset: eblaeo.pg.presets[eblaeo.pg.currentPresetType],
});
pg_selectGame(pgGame, false);
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.searchGamesResultsEl, 'afterend', 'danger', err.message);
} else {
showAlert(eblaeo.pg.searchGamesResultsEl, 'afterend', 'danger', 'failed to select game');
}
}
};
/**
* Generates the post.
*/
const pg_generatePost = () => {
if (
!eblaeo.pg.postField ||
!eblaeo.pg.previewButton ||
!eblaeo.pg.gameInfos ||
!eblaeo.pg.modalEl
) {
return;
}
try {
const divEl = document.createElement('div');
const elArrays = [];
let boxElArrays = [];
for (const gameInfo of eblaeo.pg.gameInfos) {
if (gameInfo.game.preset.type === 'box') {
boxElArrays.push(...gameInfo.elArrays);
} else {
if (boxElArrays.length > 0) {
// prettier-ignore
elArrays.push(
['ul', {
className: 'games',
style: {
minHeight: '0',
},
}, boxElArrays]
);
boxElArrays = [];
}
elArrays.push(...gameInfo.elArrays);
}
}
if (boxElArrays.length > 0) {
// prettier-ignore
elArrays.push(
['ul', {
className: 'games',
style: {
minHeight: '0',
},
}, boxElArrays]
);
}
DOM.insertElements(divEl, 'atinner', elArrays);
eblaeo.pg.postField.value = `${eblaeo.pg.postField.value}\n\n${divEl.innerHTML}\n\n`;
eblaeo.pg.postField.dispatchEvent(new Event('input', { bubbles: true }));
eblaeo.pg.previewButton.dispatchEvent(new Event('click', { bubbles: true }));
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.modalEl, 'beforeend', 'danger', err.message);
} else {
showAlert(eblaeo.pg.modalEl, 'beforeend', 'danger', 'failed to generate post');
}
}
};
/**
* Selects a game.
* @param {PgGame} game The game to select.
* @param {boolean} isEdit Whether the game is being edited or not.
*/
const pg_selectGame = (game, isEdit) => {
if (
!eblaeo.pg.searchGamesField ||
!eblaeo.pg.searchGamesResultsEl ||
!eblaeo.pg.generatorEl ||
!eblaeo.pg.presetTabNavEls ||
!eblaeo.pg.presetTabEls ||
!eblaeo.pg.gamePreviewContainerEl ||
!eblaeo.pg.gamePreviewBodyEl ||
!eblaeo.pg.gamePreviewButton ||
!eblaeo.pg.fullPreviewEl
) {
return;
}
try {
eblaeo.pg.currentPresetType = game.preset.type || 'box';
const presetTabNavElEntries = /** @type {[PgPresetType, HTMLElement | null][]} */ (Object.entries(
eblaeo.pg.presetTabNavEls
));
for (const [presetType, presetTabNavEl] of presetTabNavElEntries) {
const presetTabEl = eblaeo.pg.presetTabEls[presetType];
if (!presetTabNavEl || !presetTabEl) {
continue;
}
if (presetType === eblaeo.pg.currentPresetType) {
presetTabNavEl.classList.add('active');
presetTabEl.classList.add('active');
} else {
presetTabNavEl.classList.remove('active');
presetTabEl.classList.remove('active');
}
}
eblaeo.pg.selectedGame = game;
eblaeo.pg.isEditing = isEdit;
eblaeo.pg.searchGamesField.value = '';
eblaeo.pg.searchGamesResultsEl.innerHTML = '';
eblaeo.pg.generatorEl.style.display = 'block';
eblaeo.pg.gamePreviewContainerEl.style.display = 'block';
eblaeo.pg.gamePreviewBodyEl.innerHTML = '';
eblaeo.pg.gamePreviewButton.textContent = isEdit ? 'Edit' : 'Add';
eblaeo.pg.fullPreviewEl.style.display = 'none';
Object.entries(game.preset.prefs).forEach((entry) =>
// @ts-expect-error
pg_fillPresetField(game.preset.type, entry)
);
// @ts-expect-error
Object.entries(game).forEach(pg_fillField);
eblaeo.pg.gamePreviewButton.onclick = () => pg_generateGame(game, isEdit, false);
pg_gamePreview();
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.generatorEl, 'beforebegin', 'danger', err.message);
} else {
showAlert(eblaeo.pg.generatorEl, 'beforebegin', 'danger', 'failed to select game');
}
}
};
/**
* Generates a game.
* @param {PgGame} game The game to generate.
* @param {boolean} isEdit Whether the game is being edited or not.
* @param {boolean} isFromCache Whether the game is from the cache or not.
*/
const pg_generateGame = async (game, isEdit, isFromCache) => {
if (
!eblaeo.pg.gameInfos ||
!eblaeo.pg.generatorEl ||
!eblaeo.pg.gamePreviewContainerEl ||
!eblaeo.pg.gamePreviewButton ||
!eblaeo.pg.fullPreviewEl
) {
return;
}
try {
if (!isFromCache) {
eblaeo.pg.gamePreviewButton.textContent = isEdit ? 'Editing...' : 'Adding...';
}
const gameElArrays = await pg_getGame(game, isFromCache);
if (!gameElArrays) {
throw new CustomError('could not build elements for game generation');
}
if (isEdit) {
const gameInfo = eblaeo.pg.gameInfos.find((gameInfo) => gameInfo.game === game);
if (gameInfo) {
gameInfo.elArrays = gameElArrays;
}
} else {
eblaeo.pg.gameInfos.push({ game, elArrays: gameElArrays });
}
if (isFromCache) {
return;
}
pg_saveCaches();
eblaeo.pg.isEditing = false;
eblaeo.pg.generatorEl.style.display = 'none';
eblaeo.pg.gamePreviewContainerEl.style.display = 'none';
eblaeo.pg.fullPreviewEl.style.display = 'block';
pg_fullPreview();
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.generatorEl, 'afterend', 'danger', err.message);
} else {
showAlert(eblaeo.pg.generatorEl, 'afterend', 'danger', 'failed to generate game');
}
throw err;
}
};
/**
* Returns element arrays for a game.
* @param {PgGame} game The game.
* @param {boolean} isFromCache Whether the game is from the cache or not.
* @returns {Promise<ElementArray[] | undefined>} The element arrays for the game, if successful.
*/
const pg_getGame = async (game, isFromCache) => {
if (!isFromCache) {
pg_fillGameValues(game);
}
game.playtime.thisMonth = await pg_getPlaytimeThisMonth(game);
game.screenshotsCount = await pg_getScreenshotsCount(game);
const reviewPreviewEl = await pg_getReviewPreview(game);
if (pg_isPresetType(game.preset, 'box')) {
return pg_getBoxGame(game, game.preset, reviewPreviewEl);
}
if (pg_isPresetType(game.preset, 'bar')) {
return pg_getBarGame(game, game.preset, reviewPreviewEl);
}
if (pg_isPresetType(game.preset, 'panel')) {
return pg_getPanelGame(game, game.preset, reviewPreviewEl);
}
if (pg_isPresetType(game.preset, 'custom')) {
return pg_getCustomGame(game, game.preset, reviewPreviewEl);
}
};
/**
* Fills the values for a game from the field values.
* @param {PgGame} game The game.
*/
const pg_fillGameValues = (game) => {
if (!eblaeo.pg.fields) {
return;
}
const fieldKeys = /** @type {(keyof PgFields)[]} */ (Object.keys(eblaeo.pg.fields));
for (const fieldKey of fieldKeys) {
if (fieldKey === 'presets') {
const presetKeys = /** @type {PgPresetFieldKey[]} */ (Object.keys(
eblaeo.pg.fields.presets[game.preset.type]
));
for (const presetKey of presetKeys) {
// @ts-expect-error
game.preset.prefs[presetKey] = /** @type {never} */ (pg_getPresetFieldValue(
game.preset.type,
presetKey
));
}
} else if (fieldKey !== 'presetName') {
game[fieldKey] = /** @type {never} */ (pg_getFieldValue(fieldKey));
}
}
pg_handleFieldDependencies(game);
if (!pg_isNotPresetType(game.preset, 'custom')) {
return;
}
const oldPlaytimeTemplate = game.preset.prefs.playtimeTemplate;
if (game.preset.prefs.showPlaytimeThisMonth) {
if (!game.preset.prefs.playtimeTemplate.includes('%playtime_this_month%')) {
game.preset.prefs.playtimeTemplate = `${game.preset.prefs.playtimeTemplate} (%playtime_this_month% this month)`;
}
} else {
game.preset.prefs.playtimeTemplate = game.preset.prefs.playtimeTemplate.replace(
' (%playtime_this_month% this month)',
''
);
}
const newPlaytimeTemplate = game.preset.prefs.playtimeTemplate;
if (newPlaytimeTemplate !== oldPlaytimeTemplate) {
pg_fillPresetField(game.preset.type, ['playtimeTemplate', newPlaytimeTemplate]);
}
};
/**
* Handles field dependencies for a game.
* @param {PgGame} game The game.
*/
const pg_handleFieldDependencies = (game) => {
if (!eblaeo.pg.fields) {
return;
}
const fieldKeys = /** @type {(keyof PgFields)[]} */ (Object.keys(eblaeo.pg.fields));
for (const fieldKey of fieldKeys) {
if (fieldKey === 'presets') {
const presetKeys = /** @type {PgPresetFieldKey[]} */ (Object.keys(
eblaeo.pg.fields.presets[game.preset.type]
));
for (const presetKey of presetKeys) {
/** @type {boolean} */
let shouldBeVisible;
switch (presetKey) {
case 'checkScreenshots':
// @ts-expect-error
shouldBeVisible = game.preset.prefs.checkScreenshots;
pg_togglePresetField(game.preset.type, 'linkScreenshots', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'screenshotsTemplate', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'noScreenshotsTemplate', shouldBeVisible);
break;
case 'usePredefinedTheme':
// @ts-expect-error
shouldBeVisible = game.preset.prefs.usePredefinedTheme;
pg_togglePresetField(game.preset.type, 'predefinedThemeColor', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgType', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgColor1', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgColor2', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'titleColor', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'textColor', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'linkColor', !shouldBeVisible);
break;
case 'useCustomTheme':
// @ts-expect-error
shouldBeVisible = game.preset.prefs.useCustomTheme;
pg_togglePresetField(game.preset.type, 'predefinedThemeColor', !shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgType', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgColor1', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'bgColor2', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'titleColor', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'textColor', shouldBeVisible);
pg_togglePresetField(game.preset.type, 'linkColor', shouldBeVisible);
break;
case 'bgType':
shouldBeVisible =
// @ts-expect-error
!game.preset.prefs.usePredefinedTheme && game.preset.prefs.bgType !== 'Solid';
pg_togglePresetField(game.preset.type, 'bgColor2', shouldBeVisible);
break;
case 'useCollapsibleReview':
// @ts-expect-error
shouldBeVisible = game.preset.prefs.useCollapsibleReview;
pg_togglePresetField(game.preset.type, 'reviewTriggerMethod', shouldBeVisible);
break;
// no default
}
}
} else if (fieldKey !== 'presetName') {
// Do nothing for now.
}
}
};
/**
* Fills a preset field
* @param {PgPresetType} presetType The preset type.
* @param {[PgPresetFieldKey, unknown]} pair The key / value pair to fill.
*/
const pg_fillPresetField = (presetType, [key, value]) => {
if (!eblaeo.pg.fields) {
return;
}
// @ts-expect-error
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fields.presets[presetType][
key
]);
if (!field) {
return;
}
if (field.type === 'checkbox' || field.type === 'radio') {
// @ts-expect-error
field.checked = value;
} else {
// @ts-expect-error
field.value = value;
}
};
/**
* Fills a field
* @param {[PgFieldKey, unknown]} pair The key / value pair to fill.
*/
const pg_fillField = ([key, value]) => {
if (!eblaeo.pg.fields) {
return;
}
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fields[key]);
if (!field) {
return;
}
if (field.type === 'checkbox' || field.type === 'radio') {
// @ts-expect-error
field.checked = value;
} else {
// @ts-expect-error
field.value = value;
}
};
/**
* Returns a preset field value.
* @param {PgPresetType} presetType The preset type.
* @param {PgPresetFieldKey} key The field key.
* @returns {unknown} The field value.
*/
const pg_getPresetFieldValue = (presetType, key) => {
if (!eblaeo.pg.fields) {
return;
}
// @ts-expect-error
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fields.presets[presetType][
key
]);
return field
? field.type === 'checkbox' || field.type === 'radio'
? field.checked
: field.value
: null;
};
/**
* Returns a field value.
* @param {PgFieldKey} key The field key.
* @returns {unknown} The field value.
*/
const pg_getFieldValue = (key) => {
if (!eblaeo.pg.fields) {
return '';
}
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fields[key]);
return field
? field.type === 'checkbox' || field.type === 'radio'
? field.checked
: field.value
: null;
};
/**
* Toggles a preset field visibility.
* @param {PgPresetType} presetType The preset type.
* @param {PgPresetFieldKey} key The field key.
* @param {boolean} shouldBeVisible Whether the field should be visible or not.
*/
const pg_togglePresetField = (presetType, key, shouldBeVisible) => {
if (!eblaeo.pg.fieldContainerEls) {
return;
}
// @ts-expect-error
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fieldContainerEls.presets[
presetType
][key]);
if (field) {
field.style.display = shouldBeVisible ? 'block' : 'none';
}
};
/**
* Toggles a field visibility.
* @param {PgFieldKey} key The field key.
* @param {boolean} shouldBeVisible Whether the field should be visible or not.
*/
// eslint-disable-next-line
const pg_toggleField = (key, shouldBeVisible) => {
if (!eblaeo.pg.fieldContainerEls) {
return '';
}
const field = /** @type {HTMLInputElement | null} */ (eblaeo.pg.fieldContainerEls[key]);
if (field) {
field.style.display = shouldBeVisible ? 'block' : 'none';
}
};
/**
* Retrieves the playtime for a game this month.
* @param {PgGame} game The game.
* @returns {Promise<number>} The playtime for the game this month.
*/
const pg_getPlaytimeThisMonth = async (game) => {
if (
(pg_isPresetType(game.preset, 'custom') &&
!game.preset.prefs.htmlTemplate.includes('%playtime_this_month%')) ||
(pg_isNotPresetType(game.preset, 'custom') && !game.preset.prefs.showPlaytimeThisMonth)
) {
return 0;
}
if (!eblaeo.pg.playtimeThisMonthCache) {
const recentlyPlayed =
(await BlaeoApi.getRecentlyPlayed({ steamId: eblaeo.user.steamId })) || [];
eblaeo.pg.playtimeThisMonthCache = Object.fromEntries(
recentlyPlayed.map((recentlyPlayedGame) => [
recentlyPlayedGame.steam_id,
recentlyPlayedGame.minutes,
])
);
}
return eblaeo.pg.playtimeThisMonthCache[game.id] || 0;
};
/**
* Retrieves the screenshots count for a game.
* @param {PgGame} game The game.
* @returns {Promise<number>} The screenshots count for the game.
*/
const pg_getScreenshotsCount = async (game) => {
if (
(pg_isPresetType(game.preset, 'custom') &&
!game.preset.prefs.htmlTemplate.includes('%screenshots%') &&
!game.preset.prefs.htmlTemplate.includes('%screenshots_count%')) ||
(pg_isNotPresetType(game.preset, 'custom') && !game.preset.prefs.checkScreenshots)
) {
return 0;
}
if (!eblaeo.pg.screenshotsCache) {
eblaeo.pg.screenshotsCache = {};
}
if (!Utils.isSet(eblaeo.pg.screenshotsCache[game.id])) {
const response = await Requests.GET(
`https://steamcommunity.com/profiles/${eblaeo.user.steamId}/screenshots?appid=${game.id}`
);
if (response.dom) {
const elements = Array.from(
response.dom.querySelectorAll('[href*="steamcommunity.com/sharedfiles/filedetails"]')
);
eblaeo.pg.screenshotsCache[game.id] = elements.length;
}
}
return eblaeo.pg.screenshotsCache[game.id] || 0;
};
/**
* Retrieves the review preview for a game.
* @param {PgGame} game The game.
* @returns {Promise<HTMLElement | null>} The element of the review preview for the game, if successful.
*/
const pg_getReviewPreview = async (game) => {
if (!game.review) {
return null;
}
if (!eblaeo.pg.reviewsCache) {
eblaeo.pg.reviewsCache = {};
}
let reviewCache = eblaeo.pg.reviewsCache[game.id];
if (!reviewCache || reviewCache.review !== game.review) {
const reviewPreviewEl =
(await BlaeoApi.previewPost(
{ steamId: eblaeo.user.steamId },
pg_replacePlaceholders(game.review, game)
)) || null;
if (reviewPreviewEl) {
eblaeo.pg.reviewsCache[game.id] = {
review: game.review,
reviewPreview: reviewPreviewEl.outerHTML,
};
reviewCache = eblaeo.pg.reviewsCache[game.id];
}
}
if (!reviewCache) {
return null;
}
const divEl = document.createElement('div');
divEl.innerHTML = reviewCache.reviewPreview;
return /** @type {HTMLElement | null} */ (divEl.firstElementChild);
};
/**
* Returns element arrays for a game using a box preset.
* @param {PgGame} game The game.
* @param {PgPreset<'box'>} preset The box preset to use.
* @param {HTMLElement | null} reviewPreviewEl The element of the review preview for the game, if any.
* @returns {ElementArray[]} The element arrays for the game.
*/
const pg_getBoxGame = (game, preset, reviewPreviewEl) => {
// prettier-ignore
let elArrays = /** @type {ElementArray[]} */ ([
['li', {
className: `game game-thumbnail game-${game.progress}`,
style: {
background:
preset.prefs.bgType === 'Solid'
? null
: `linear-gradient(to ${
preset.prefs.bgType === 'Horizontal gradient' ? 'right' : 'bottom'
}, ${preset.prefs.bgColor1}, ${preset.prefs.bgColor2})`,
backgroundColor: preset.prefs.bgType === 'Solid' ? preset.prefs.bgColor1 : null,
color: preset.prefs.textColor,
},
}, [
['div', {
className: 'title',
style: {
color: preset.prefs.titleColor,
},
}, game.name],
['a', { href: `https://store.steampowered.com/app/${game.id}/`, target: '_blank' }, [
['img', { src: game.image, alt: game.name }, null],
]],
['div', {
className: 'caption',
style: {
backgroundColor: 'transparent',
color: 'inherit',
height: 'auto',
padding: '9px',
},
}, [
['p', null, pg_replacePlaceholders(preset.prefs.playtimeTemplate, game)],
game.achievements.total === 0
? ['p', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noAchievementsTemplate, game)]
: preset.prefs.linkAchievements
? ['p', null, [
['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/stats/%id%/?tab=achievements',
game
),
target: '_blank',
style: {
color: preset.prefs.linkColor,
},
}, pg_replacePlaceholders(preset.prefs.achievementsTemplate, game)],
]]
: ['p', null, pg_replacePlaceholders(preset.prefs.achievementsTemplate, game)],
preset.prefs.checkScreenshots
? (
game.screenshotsCount === 0
? ['p', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noScreenshotsTemplate, game)]
: preset.prefs.linkScreenshots
? ['p', null, [
['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/screenshots?appid=%id%',
game
),
target: '_blank',
style: {
color: preset.prefs.linkColor,
},
}, pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game)],
]]
: ['p', null, pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game)]
)
: null,
]],
]],
]);
if (!reviewPreviewEl) {
return elArrays;
}
// prettier-ignore
elArrays = [
['div', {
style: {
overflow: 'auto',
},
}, [
['div', {
style: {
...(
preset.prefs.reviewPosition === 'Left'
? {
float: 'right',
margin: '5px 5px 5px 10px',
}
: {
float: 'left',
margin: '5px 10px 5px 5px',
}
),
position: 'relative',
zIndex: '1',
},
}, elArrays],
['div', {
style: {
fontSize: '14px',
textAlign: 'justify',
},
}, reviewPreviewEl],
]],
];
return elArrays;
};
/**
* Returns element arrays for a game using a bar preset.
* @param {PgGame} game The game.
* @param {PgPreset<'bar'>} preset The bar preset to use.
* @param {HTMLElement | null} reviewPreviewEl The element of the review preview for the game, if any.
* @returns {ElementArray[]} The element arrays for the game.
*/
const pg_getBarGame = (game, preset, reviewPreviewEl) => {
const showInfoInOneLine = preset.prefs.showInfoInOneLine || !!game.customHtml;
let customEls;
if (game.customHtml) {
const divEl = document.createElement('div');
divEl.innerHTML = game.customHtml;
customEls = Array.from(divEl.childNodes).map((child) =>
child.nodeType === Node.TEXT_NODE ? child.textContent : child
);
}
const reviewId = `review-${eblaeo.user.username}-${game.id}`;
// prettier-ignore
const imageArray = /** @type {ElementArray} */ (
['div', {
className: `media-${preset.prefs.imagePosition.toLowerCase()}`,
style: {
padding: '0',
},
}, [
['a', { href: `https://store.steampowered.com/app/${game.id}/`, target: '_blank' }, [
['img', {
src: game.image,
alt: game.name,
style: {
maxWidth: 'unset',
},
}, null],
]],
]]
);
// prettier-ignore
const barArray = /** @type {ElementArray} */ (
['div', {
className: 'media-body',
style: {
fontSize: showInfoInOneLine ? '12px' : null,
padding: '0 10px',
position: 'relative',
},
}, [
['h4', {
className: 'media-heading',
style: {
color: preset.prefs.titleColor,
},
}, game.name],
pg_replacePlaceholders(preset.prefs.playtimeTemplate, game),
showInfoInOneLine ? ', ' : ['br', null, null],
game.achievements.total === 0
? ['span', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noAchievementsTemplate, game)]
: preset.prefs.linkAchievements
? ['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/stats/%id%/?tab=achievements',
game
),
target: '_blank',
style: {
color: preset.prefs.linkColor,
},
}, pg_replacePlaceholders(preset.prefs.achievementsTemplate, game)]
: pg_replacePlaceholders(preset.prefs.achievementsTemplate, game),
...(
preset.prefs.checkScreenshots
? [
', ',
game.screenshotsCount === 0
? ['span', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noScreenshotsTemplate, game)]
: preset.prefs.linkScreenshots
? ['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/screenshots?appid=%id%',
game
),
target: '_blank',
style: {
color: preset.prefs.linkColor,
},
}, pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game)]
: pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game),
]
: []
),
...(
customEls
? [
['br', null, null],
...customEls,
]
: []
),
preset.prefs.useCollapsibleReview && reviewPreviewEl
? (
preset.prefs.reviewTriggerMethod === 'Bar click'
? ['span', {
style: {
bottom: '50%',
fontSize: '14px',
fontWeight: 'bold',
position: 'absolute',
right: '10px',
transform: 'translateY(50%)',
},
}, [
'More ',
['i', { className: 'fa fa-level-down' }, null],
]]
: ['button', {
className: 'btn btn-xs',
dataset: { toggle: 'collapse', target: `#${reviewId}` },
style: {
backgroundColor: preset.prefs.titleColor,
bottom: '50%',
color: preset.prefs.bgColor1,
fontSize: '14px',
fontWeight: 'bold',
position: 'absolute',
right: '10px',
transform: 'translateY(50%)',
},
}, [
'More ',
['i', { className: 'fa fa-level-down' }, null],
]]
)
: null,
]]
);
// prettier-ignore
return /** @type {ElementArray[]} */ ([
['div', {
className: `game game-media game-${game.progress}`,
dataset:
preset.prefs.useCollapsibleReview &&
preset.prefs.reviewTriggerMethod === 'Bar click' &&
reviewPreviewEl
? { toggle: 'collapse', target: `#${reviewId}` }
: null,
style: {
background:
preset.prefs.bgType === 'Solid'
? null
: `linear-gradient(to ${
preset.prefs.bgType === 'Horizontal gradient' ? 'right' : 'bottom'
}, ${preset.prefs.bgColor1}, ${preset.prefs.bgColor2})`,
backgroundColor: preset.prefs.bgType === 'Solid' ? preset.prefs.bgColor1 : null,
borderLeft:
preset.prefs.completionBarPosition === 'Left'
? `10px solid ${gameCategoryInfos[game.progress].color}`
: '0',
borderRight:
preset.prefs.completionBarPosition === 'Right'
? `10px solid ${gameCategoryInfos[game.progress].color}`
: '0',
color: preset.prefs.textColor,
},
},
preset.prefs.imagePosition === 'Left' ? [imageArray, barArray] : [barArray, imageArray]
],
reviewPreviewEl
? ['div', {
...(preset.prefs.useCollapsibleReview ? { id: reviewId, className: 'collapse' } : {}),
style: {
border: '1px solid #dee2e6',
borderRadius: '0 0 4px 4px',
borderTop: '0',
padding: '10px',
textAlign: 'justify',
},
}, reviewPreviewEl]
: null,
]);
};
/**
* Returns element arrays for a game using a panel preset.
* @param {PgGame} game The game.
* @param {PgPreset<'panel'>} preset The panel preset to use.
* @param {HTMLElement | null} reviewPreviewEl The element of the review preview for the game, if any.
* @returns {ElementArray[]} The element arrays for the game.
*/
const pg_getPanelGame = (game, preset, reviewPreviewEl) => {
const reviewId = `review-${eblaeo.user.username}-${game.id}`;
// prettier-ignore
return /** @type {ElementArray[]} */ ([
['div', {
className: `panel ${
preset.prefs.usePredefinedTheme
? `panel-${bootstrapColorClasses[preset.prefs.predefinedThemeColor]}`
: ''
}`,
style: {
borderColor: preset.prefs.useCustomTheme ? preset.prefs.bgColor1 : null,
},
}, [
['div', {
className: 'panel-heading',
dataset:
preset.prefs.useCollapsibleReview && reviewPreviewEl
? { toggle: 'collapse', target: `#${reviewId}` }
: null,
style: {
background:
preset.prefs.usePredefinedTheme || preset.prefs.bgType === 'Solid'
? null
: `linear-gradient(to ${
preset.prefs.bgType === 'Horizontal gradient' ? 'right' : 'bottom'
}, ${preset.prefs.bgColor1}, ${preset.prefs.bgColor2})`,
backgroundColor:
preset.prefs.useCustomTheme && preset.prefs.bgType === 'Solid'
? preset.prefs.bgColor1
: null,
},
}, [
['div', {
className: `game game-media game-${game.progress}`,
style: {
color: preset.prefs.useCustomTheme ? preset.prefs.textColor : null,
},
}, [
['div', { className: 'media-left' }, [
['a', { href: `https://store.steampowered.com/app/${game.id}/`, target: '_blank' }, [
['img', {
src: `https://steamcdn-a.akamaihd.net/steam/apps/${game.id}/header.jpg`,
alt: game.name,
style: {
height: '90px',
maxWidth: 'unset',
width: 'unset',
},
}, null],
]],
]],
['div', {
className: 'media-body',
style: {
position: 'relative',
},
}, [
['h4', {
className: 'media-heading',
style: {
color: preset.prefs.useCustomTheme ? preset.prefs.titleColor : null,
},
}, [
game.name,
' ',
['a', {
href: `https://store.steampowered.com/app/${game.id}`,
target: '_blank',
style: {
color: preset.prefs.useCustomTheme ? preset.prefs.linkColor : null,
},
}, [
['font', { size: '2px' }, [
['i', { className: 'fa fa-external-link' }, null],
]],
]],
preset.prefs.checkScreenshots && game.rating
? ['div', {
style: {
float: 'right',
},
}, [
`${game.rating} `,
['i', { className: 'fa fa-star' }, null],
]]
: null,
]],
!preset.prefs.checkScreenshots && game.rating
? ['div', null, [
['i', { className: 'fa fa-star' }, null],
` ${game.rating}`,
]]
: null,
!preset.prefs.checkScreenshots && !game.rating
? ['br', null, null]
: null,
['div', null, [
['i', { className: 'fa fa-clock-o' }, null],
` ${pg_replacePlaceholders(preset.prefs.playtimeTemplate, game)}`,
]],
['div', null, [
['i', { className: 'fa fa-trophy' }, null],
' ',
game.achievements.total === 0
? ['span', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noAchievementsTemplate, game)]
: preset.prefs.linkAchievements
? ['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/stats/%id%/?tab=achievements',
game
),
target: '_blank',
style: {
color: preset.prefs.useCustomTheme ? preset.prefs.linkColor : null,
},
}, pg_replacePlaceholders(preset.prefs.achievementsTemplate, game)]
: pg_replacePlaceholders(preset.prefs.achievementsTemplate, game)
]],
preset.prefs.checkScreenshots
? ['div', null, [
['i', { className: 'fa fa-image' }, null],
' ',
game.screenshotsCount === 0
? ['span', {
style: {
color: 'inherit',
opacity: '0.5',
},
}, pg_replacePlaceholders(preset.prefs.noScreenshotsTemplate, game)]
: preset.prefs.linkScreenshots
? ['a', {
href: pg_replacePlaceholders(
'https://steamcommunity.com/profiles/%steamid%/screenshots?appid=%id%',
game
),
target: '_blank',
style: {
color: preset.prefs.useCustomTheme ? preset.prefs.linkColor : null,
},
}, pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game)]
: pg_replacePlaceholders(preset.prefs.screenshotsTemplate, game)
]]
: null,
preset.prefs.useCollapsibleReview && reviewPreviewEl
? ['span', {
style: {
bottom: '1px',
fontWeight: 'bold',
position: 'absolute',
right: '10px',
},
}, [
'More ',
['i', { className: 'fa fa-level-down' }, null],
]]
: null,
]],
]],
]],
reviewPreviewEl
? ['div', {
...(preset.prefs.useCollapsibleReview ? { id: reviewId, className: 'collapse' } : {}),
style: {
padding: '10px',
textAlign: 'justify',
},
}, reviewPreviewEl]
: null,
]],
]);
};
/**
* Returns element arrays for a game using a custom preset.
* @param {PgGame} game The game.
* @param {PgPreset<'custom'>} preset The custom preset to use.
* @param {HTMLElement | null} reviewPreviewEl The element of the review preview for the game, if any.
* @returns {ElementArray[]} The element arrays for the game.
*/
const pg_getCustomGame = (game, preset, reviewPreviewEl) => {
const divEl = document.createElement('div');
divEl.innerHTML = pg_replacePlaceholders(preset.prefs.htmlTemplate, game);
if (reviewPreviewEl) {
const reviewId = `review-${eblaeo.user.username}-${game.id}`;
const reviewEl = divEl.querySelector(`#${reviewId}`);
if (reviewEl) {
reviewEl.appendChild(reviewPreviewEl);
}
}
return Array.from(divEl.childNodes).map((child) =>
child.nodeType === Node.TEXT_NODE ? child.textContent : child
);
};
/**
* Replaces placeholders in a text.
* @param {string} text The text where to replace the placeholders.
* @param {PgGame} game The game with the values to replace the placeholders.
* @returns {string} The text with the placeholders replaced.
*/
const pg_replacePlaceholders = (text, game) => {
const achievementsPercentage =
game.achievements.total > 0
? Math.round((game.achievements.unlocked / game.achievements.total) * 10000) / 100
: 0;
return (text || '')
.replace(/%steamid%/g, eblaeo.user.steamId)
.replace(/%username%/g, eblaeo.user.username)
.replace(/%id%/g, game.id.toString())
.replace(/%name%/g, game.name)
.replace(/%image%/g, game.image)
.replace(/%progress%/g, game.progress)
.replace(/%progress_name%/g, gameCategoryInfos[game.progress].name)
.replace(/%progress_color%/g, gameCategoryInfos[game.progress].color)
.replace(
/%playtime%/g,
Utils.getRelativeTimeFromMinutes(game.playtime.total, 'h').replace('about ', '')
)
.replace(
/%playtime_this_month%/g,
Utils.getRelativeTimeFromMinutes(game.playtime.thisMonth, 'h').replace('about ', '')
)
.replace(
/%achievements%/g,
game.achievements.total > 0
? `${game.achievements.unlocked} of ${game.achievements.total} achievements`
: 'no achievements'
)
.replace(/%achievements_unlocked%/g, game.achievements.unlocked.toLocaleString())
.replace(/%achievements_total%/g, game.achievements.total.toLocaleString())
.replace(/%achievements_percentage%/g, achievementsPercentage.toLocaleString())
.replace(
/%screenshots%/g,
game.screenshotsCount > 0 ? `${game.screenshotsCount} screenshots` : 'no screenshots'
)
.replace(/%screenshots_count%/g, game.screenshotsCount.toLocaleString());
};
/**
* @template {PgPresetType} T
* @param {PgPreset<PgPresetType>} preset
* @param {T} type
* @returns {preset is PgPreset<T>}
*/
const pg_isPresetType = (preset, type) => {
return preset.type === type;
};
/**
* @template {PgPresetType} T
* @param {PgPreset<PgPresetType>} preset
* @param {T} type
* @returns {preset is PgPreset<Exclude<PgPresetType, T>>}
*/
const pg_isNotPresetType = (preset, type) => {
return preset.type !== type;
};
/**
* Previews the selected game.
* @returns {Promise<void>}
*/
const pg_gamePreview = async () => {
if (
!eblaeo.pg.gameInfos ||
!eblaeo.pg.selectedGame ||
!eblaeo.pg.gamePreviewContainerEl ||
!eblaeo.pg.gamePreviewBodyEl
) {
return;
}
try {
eblaeo.pg.gamePreviewContainerEl.style.display = 'block';
showAlert(eblaeo.pg.gamePreviewBodyEl, 'atinner', 'loading', 'Loading game preview...');
const gameElArrays = await pg_getGame(eblaeo.pg.selectedGame, false);
if (!gameElArrays) {
throw new CustomError('could not build elements for game preview');
}
// prettier-ignore
DOM.insertElements(eblaeo.pg.gamePreviewBodyEl, 'atinner',
eblaeo.pg.selectedGame.preset.type === 'box'
? [
['ul', {
className: 'games',
style: {
minHeight: '0',
},
}, gameElArrays]
]
: gameElArrays
);
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.gamePreviewBodyEl, 'atinner', 'danger', err.message);
} else {
showAlert(eblaeo.pg.gamePreviewBodyEl, 'atinner', 'danger', 'failed to preview game');
}
}
};
/**
* Previews all games.
*/
const pg_fullPreview = () => {
if (!eblaeo.pg.gameInfos || !eblaeo.pg.fullPreviewEl || !eblaeo.pg.fullPreviewBodyEl) {
return;
}
try {
eblaeo.pg.fullPreviewEl.style.display = 'block';
showAlert(eblaeo.pg.fullPreviewBodyEl, 'atinner', 'loading', 'Loading full preview...');
const elArrays = [];
let boxElArrays = [];
for (const gameInfo of eblaeo.pg.gameInfos) {
if (gameInfo.game.preset.type === 'box') {
boxElArrays.push(pg_getFullPreviewGame(gameInfo));
} else {
if (boxElArrays.length > 0) {
// prettier-ignore
elArrays.push(
['ul', {
className: 'games',
style: {
minHeight: '0',
},
}, boxElArrays]
);
boxElArrays = [];
}
elArrays.push(pg_getFullPreviewGame(gameInfo));
}
}
if (boxElArrays.length > 0) {
// prettier-ignore
elArrays.push(
['ul', {
className: 'games',
style: {
minHeight: '0',
},
}, boxElArrays]
);
}
DOM.insertElements(eblaeo.pg.fullPreviewBodyEl, 'atinner', elArrays);
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.fullPreviewBodyEl, 'atinner', 'danger', err.message);
} else {
showAlert(eblaeo.pg.fullPreviewBodyEl, 'atinner', 'danger', 'failed to preview all games');
}
}
};
/**
* Returns an element array for a full preview game.
* @param {PgGameInfo} gameInfo The info for the game.
* @returns {ElementArray} The element array for the game.
*/
const pg_getFullPreviewGame = (gameInfo) => {
// prettier-ignore
return /** @type {ElementArray} */ (
['div', {
className: 'eblaeo-pg-full-preview-game',
style: {
display: gameInfo.game.preset.type === 'box' ? 'inline-block' : null,
},
}, [
...gameInfo.elArrays,
['div', { className: 'btn-toolbar' }, [
['button', {
type: 'button',
className: 'btn btn-default edit',
title: 'Edit',
onclick: () => pg_selectGame(gameInfo.game, true),
}, [
['i', { className: 'fa fa-edit' }, null],
]],
['button', {
type: 'button',
className: 'btn btn-default remove',
title: 'Remove',
onclick: () => pg_removeGame(gameInfo.game),
}, [
['i', { className: 'fa fa-trash' }, null],
]],
['button', {
type: 'button',
className: 'btn btn-default move-down',
title: 'Move down',
onclick: () => pg_moveGameDown(gameInfo.game),
}, [
['i', { className: 'fa fa-arrow-down' }, null],
]],
['button', {
type: 'button',
className: 'btn btn-default move-up',
title: 'Move up',
onclick: () => pg_moveGameUp(gameInfo.game),
}, [
['i', { className: 'fa fa-arrow-up' }, null],
]],
]],
]]
);
};
/**
* Removes a game.
* @param {PgGame} game The game to remove.
*/
const pg_removeGame = (game) => {
showDialog('Are you sure you want to remove this game?', () => {
if (!eblaeo.pg.gameInfos) {
return;
}
eblaeo.pg.gameInfos = eblaeo.pg.gameInfos.filter((gameInfo) => gameInfo.game !== game);
pg_saveCaches();
pg_fullPreview();
});
};
/**
* Moves a game down.
* @param {PgGame} game The game to move down.
*/
const pg_moveGameDown = (game) => {
if (!eblaeo.pg.gameInfos) {
return;
}
const currentIndex = eblaeo.pg.gameInfos.findIndex((gameInfo) => gameInfo.game === game);
const nextIndex = currentIndex + 1;
if (nextIndex >= eblaeo.pg.gameInfos.length) {
showDialog('Cannot move down!');
return;
}
const tmpGameInfo = eblaeo.pg.gameInfos[nextIndex];
eblaeo.pg.gameInfos[nextIndex] = eblaeo.pg.gameInfos[currentIndex];
eblaeo.pg.gameInfos[currentIndex] = tmpGameInfo;
pg_saveCaches();
pg_fullPreview();
};
/**
* Moves a game up.
* @param {PgGame} game The game to move up.
*/
const pg_moveGameUp = (game) => {
if (!eblaeo.pg.gameInfos) {
return;
}
const currentIndex = eblaeo.pg.gameInfos.findIndex((gameInfo) => gameInfo.game === game);
const previousIndex = currentIndex - 1;
if (previousIndex < 0) {
showDialog('Cannot move up!');
return;
}
const tmpGameInfo = eblaeo.pg.gameInfos[previousIndex];
eblaeo.pg.gameInfos[previousIndex] = eblaeo.pg.gameInfos[currentIndex];
eblaeo.pg.gameInfos[currentIndex] = tmpGameInfo;
pg_saveCaches();
pg_fullPreview();
};
/**
* Loads the caches.
* @returns {Promise<void>}
*/
const pg_loadCaches = async () => {
if (!eblaeo.pg.generatorEl) {
return;
}
eblaeo.pg.gamesCache = /** @type {PgGame[]} */ (PersistentStorage.getLocalValue('gamesCache'));
eblaeo.pg.playtimeThisMonthCache = /** @type {Record<number, number>} */ (PersistentStorage.getLocalValue(
'playtimeThisMonthCache'
));
eblaeo.pg.screenshotsCache = /** @type {Record<number, number>} */ (PersistentStorage.getLocalValue(
'screenshotsCache'
));
eblaeo.pg.reviewsCache = /** @type {Record<number, PgReviewCache>} */ (PersistentStorage.getLocalValue(
'reviewsCache'
));
if (!eblaeo.pg.gamesCache || eblaeo.pg.gamesCache.length === 0) {
return;
}
try {
for (const game of eblaeo.pg.gamesCache) {
await pg_generateGame(game, false, true);
}
pg_fullPreview();
} catch (err) {
if (err instanceof CustomError) {
showAlert(eblaeo.pg.generatorEl, 'beforebegin', 'danger', err.message);
} else {
showAlert(eblaeo.pg.generatorEl, 'beforebegin', 'danger', 'failed to load games cache');
}
}
};
/**
* Saves the caches.
*/
const pg_saveCaches = () => {
if (!eblaeo.pg.gameInfos) {
return;
}
PersistentStorage.setLocalValue(
'gamesCache',
eblaeo.pg.gameInfos.map((gameInfo) => gameInfo.game)
);
PersistentStorage.setLocalValue(
'playtimeThisMonthCache',
eblaeo.pg.playtimeThisMonthCache || {}
);
PersistentStorage.setLocalValue('screenshotsCache', eblaeo.pg.screenshotsCache || {});
PersistentStorage.setLocalValue('reviewsCache', eblaeo.pg.reviewsCache || {});
};
/**
* Deletes the caches.
*/
const pg_deleteCaches = () => {
PersistentStorage.deleteLocalValue('gamesCache');
PersistentStorage.deleteLocalValue('playtimeThisMonthCache');
PersistentStorage.deleteLocalValue('screenshotsCache');
PersistentStorage.deleteLocalValue('reviewsCache');
};
try {
BlaeoApi.init();
await PersistentStorage.init(scriptId, defaultValues);
await load();
} catch (err) {
console.log(`Failed to load ${scriptName}: `, err);
}
})();