allow specifying custom gecko codes

This commit is contained in:
Matteias Collet 2022-02-22 03:40:14 +00:00 committed by GitHub
parent 303a3b85ce
commit 6f4044d812
8 changed files with 376 additions and 17 deletions

View file

@ -1,16 +1,32 @@
<template> <template>
<div :class="disabled ? 'button-wrapper disabled' : 'button-wrapper'"> <div
<button @click="onClick" :disabled="disabled">{{ label }}</button> :class="
disabled
? `button-wrapper disabled ${className ? className : ''}`
: `button-wrapper ${className ? className : ''}`
"
>
<button :class="small ? 'small' : ''" @click="handleClick" :disabled="disabled">
{{ label }}
</button>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
small: { type: Boolean, required: false },
className: { type: String, required: false },
disabled: { type: Boolean }, disabled: { type: Boolean },
onClick: { type: Function }, onClick: { type: Function },
label: { type: String }, label: { type: String },
}, },
methods: {
handleClick(e) {
e.stopPropagation();
this.onClick();
},
},
}; };
</script> </script>
@ -28,6 +44,10 @@ export default {
cursor: not-allowed; cursor: not-allowed;
} }
.small {
padding: 3px 7px;
}
button { button {
border: none; border: none;
outline: none; outline: none;

View file

@ -1,5 +1,13 @@
<template> <template>
<div> <div>
<CustomCodeModal
v-if="customCodeInEditMode"
:onCancel="closeCustomClodeModal"
:onSave="saveCustomCode"
:identifier="customCodeInEditMode.identifier"
:initialValue="customCodeInEditMode.value"
:initialTitle="customCodeInEditMode.title"
/>
<div class="preset-select"> <div class="preset-select">
<SelectComponent <SelectComponent
:options="getPresetOptions()" :options="getPresetOptions()"
@ -9,16 +17,37 @@
/> />
</div> </div>
<div v-for="category in codeCategories" v-bind:key="category.identifier" class="code-group"> <div v-for="category in codeCategories" v-bind:key="category.identifier" class="code-group">
<div class="category-title">{{ getCategoryTitle(category) }}</div> <div class="category-title">
<span>{{ getCategoryTitle(category) }}</span>
<ButtonComponent
:small="true"
v-if="category.identifier === 'custom'"
className="btn-add-custom-code"
label="+"
:onClick="initCustomCodeModal"
/>
</div>
<ul> <ul>
<li <li
v-for="(code, idx) in availableCodes.filter((c) => c.category === category.identifier)" v-for="(code, idx) in availableCodes.filter((c) => c.category === category.identifier)"
v-bind:key="idx" v-bind:key="code.identifier ? code.identifier : idx"
:class="code.selected ? 'checked' : code.disabled ? 'disabled' : ''" :class="code.selected ? 'checked' : code.disabled ? 'disabled' : ''"
@click="toggle(code)" @click="toggle(code)"
@mouseover="inspect(code)" @mouseover="inspect(code)"
> >
{{ getCodeTitle(code) }} <span>
{{ getCodeTitle(code) }}
</span>
<div class="code-menu">
<button
v-if="code.identifier != null && code.category === 'custom'"
type="button"
class="btn-edit-custom-code"
@click="(e) => deleteCustomCode(e, code.identifier)"
>
&#215;
</button>
</div>
</li> </li>
<li <li
v-if="category.identifier === 'loader'" v-if="category.identifier === 'loader'"
@ -40,6 +69,7 @@ import presetCategories from '../data/presetCategories.json';
export default { export default {
props: { props: {
version: { type: String },
codes: { type: Array }, codes: { type: Array },
onSelectionChanged: { type: Function }, onSelectionChanged: { type: Function },
onInspect: { type: Function }, onInspect: { type: Function },
@ -57,6 +87,8 @@ export default {
}, },
data() { data() {
return { return {
customCodes: [],
customCodeInEditMode: null,
availableCodes: [], availableCodes: [],
codeCategories, codeCategories,
presetCategories, presetCategories,
@ -138,6 +170,64 @@ export default {
} }
} }
}, },
initCustomCodeModal() {
this.customCodeInEditMode = {
identifier: btoa(new Date().toISOString()),
title: undefined,
value: undefined,
};
},
closeCustomClodeModal() {
this.customCodeInEditMode = null;
},
deleteCustomCode(e, identifier) {
e.stopPropagation();
this.customCodes = this.customCodes.filter((c) => c.identifier !== identifier);
localStorage.setItem('custom-codes', JSON.stringify(this.customCodes));
this.availableCodes = this.availableCodes.filter((c) => c.identifier !== identifier);
this.emitChangeEvent();
},
saveCustomCode(identifier, title, value) {
const updatedCode = {
identifier,
author: '-',
title: [
{
lang: 'en-US',
content: title,
},
],
description: [
{
lang: 'en-US',
content: '-',
html: '<p>-</p>',
},
],
version: '-',
date: new Date().toLocaleDateString('en-US', {
month: 'short',
day: '2-digit',
year: 'numeric',
}),
source: value,
presets: [],
category: 'custom',
dependsOn: null,
createdOnVersion: this.version,
};
this.customCodes = [
...this.customCodes.filter((c) => c.identifier !== identifier),
updatedCode,
];
localStorage.setItem('custom-codes', JSON.stringify(this.customCodes));
this.availableCodes = [
...this.availableCodes.filter((c) => c.identifier !== identifier),
{ ...updatedCode, selected: false },
];
this.closeCustomClodeModal();
},
toggle(code) { toggle(code) {
if (!code.selected && codeCategories.find((c) => c.identifier === code.category).exclusive) { if (!code.selected && codeCategories.find((c) => c.identifier === code.category).exclusive) {
for (const availableCode of this.availableCodes.filter( for (const availableCode of this.availableCodes.filter(
@ -157,7 +247,24 @@ export default {
this.emitChangeEvent(); this.emitChangeEvent();
}, },
populate() { populate() {
this.availableCodes = this.codes.map((c) => ({ ...c, selected: false })); const storedCustomCodes = localStorage.getItem('custom-codes');
if (storedCustomCodes) {
try {
const parsedCodes = JSON.parse(storedCustomCodes);
this.customCodes = parsedCodes;
} catch (err) {
this.customCodes = [];
}
} else {
this.customCodes = [];
}
this.availableCodes = [
...this.codes.map((c) => ({ ...c, selected: false })),
...this.customCodes.map((c) => ({ ...c, selected: false })),
];
this.refreshDisabledCodes(); this.refreshDisabledCodes();
}, },
inspect(code) { inspect(code) {
@ -172,13 +279,29 @@ export default {
<style scoped> <style scoped>
.category-title { .category-title {
position: relative;
color: white; color: white;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
background: #383838b5; background: #383838b5;
padding-top: 2px; padding: 2px;
padding-bottom: 2px;
margin-bottom: 0; margin-bottom: 0;
display: grid;
grid-template-columns: auto min-content;
}
.btn-add-custom-code {
min-width: unset;
width: auto;
}
.btn-edit-custom-code {
background: transparent;
border: none;
font-size: 1.2em;
font-weight: bold;
color: red;
cursor: pointer;
} }
.category-title ~ ul { .category-title ~ ul {
@ -213,10 +336,20 @@ ul li {
user-select: none; user-select: none;
outline: none; outline: none;
display: block; display: block;
min-width: 280px; overflow: hidden;
white-space: nowrap; padding-right: 5px;
padding-right: 15px; min-width: 260px;
max-width: 260px;
text-align: left; text-align: left;
position: relative;
display: grid;
grid-template-columns: auto min-content min-content;
}
ul li > span {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} }
ul li:nth-child(odd) { ul li:nth-child(odd) {
@ -275,9 +408,9 @@ li.checked::before {
background-color: #d85e55; background-color: #d85e55;
} }
@media screen and (max-width: 400px) { @media screen and (max-width: 1100px) {
ul li { ul li {
min-width: 180px; max-width: 100%;
} }
} }
</style> </style>

View file

@ -0,0 +1,166 @@
<template>
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<span @click="confirmCancel" class="btn-close">&#215;</span>
<div class="modal-body">
<div>
<label>
{{ getLabel('codeeditor.fields.title.label') }}
</label>
<input
type="text"
:placeholder="getLabel('codeeditor.fields.title.placeholder')"
v-model="title"
/>
</div>
<div>
<label> {{ getLabel('codeeditor.fields.value.label') }}* </label>
<textarea
@paste="onPaste"
@blur="onTouch"
v-model="customCode"
:class="touched && !isCodeValid() ? 'invalid' : ''"
:placeholder="getLabel('codeeditor.fields.value.placeholder')"
/>
</div>
<div>
<ButtonComponent
:disabled="!isCodeValid()"
:label="getLabel('codeeditor.save')"
:onClick="onSubmit"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// Util
import { translate } from '../i18n/localeHelper';
export default {
props: {
identifier: { type: String, required: false },
initialTitle: { type: String, required: false },
initialValue: { type: String, required: false },
onCancel: { type: Function },
onSave: { type: Function },
},
data() {
return {
title: this.initialTitle,
customCode: this.initialValue,
touched: this.initialValue != null,
};
},
methods: {
getLabel(key) {
return translate(key, this.$lang);
},
isCodeValid() {
if (!this.customCode) return false;
const formattedCode = this.customCode.replace(/(?:\r|\n|\s)/g, '');
return /^[a-fA-F0-9]+$/.test(formattedCode) && formattedCode.length % 16 === 0;
},
onTouch() {
this.touched = true;
},
confirmCancel() {
if (!confirm(translate('common.discard', this.$lang))) return;
this.onCancel();
},
onSubmit() {
this.onSave(
this.identifier,
this.title ? this.title : 'N/A',
this.customCode.replace(/[^a-zA-Z0-9]/g, ''),
);
},
onPaste(e) {
e.stopPropagation();
e.preventDefault();
const pasteContent =
e.clipboardData?.getData?.('text') || window.clipboardData?.getData?.('text');
if (!pasteContent) return;
const newCode = `${this.customCode ? this.customCode : ''} ${pasteContent}`
.replace(/(?:\t|\s)/g, '')
.replace(/(.{8})(.{8})/g, '$1 $2\r\n');
this.customCode = newCode;
},
},
};
</script>
<style scoped>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: table;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
max-height: 80vh;
overflow-y: auto;
position: relative;
width: 300px;
margin: 0px auto;
padding: 20px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
}
.btn-close {
position: absolute;
font-size: 1.2em;
right: 20px;
top: 10px;
font-weight: bold;
cursor: pointer;
user-select: none;
}
.modal-body > div:not(:last-child) {
margin-bottom: 10px;
}
.modal-body > div > label {
display: block;
color: #727272;
font-size: 0.8em;
margin-bottom: 3px;
}
.modal-body > div > input,
.modal-body > div > textarea {
box-sizing: border-box;
width: 100%;
display: block;
}
.modal-body > div > textarea {
resize: vertical;
}
.modal-body > div > textarea.invalid {
border: 1px solid red;
}
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View file

@ -27,6 +27,7 @@
<div v-if="codes && codes.length > 0"> <div v-if="codes && codes.length > 0">
<h3>{{ getLabel('headers.codelist') }}</h3> <h3>{{ getLabel('headers.codelist') }}</h3>
<CodeList <CodeList
:version="selectedVersion"
:onStageLoaderToggle="onStageLoaderToggle" :onStageLoaderToggle="onStageLoaderToggle"
:codes="codes" :codes="codes"
:onSelectionChanged="onCheatSelectionChanged" :onSelectionChanged="onCheatSelectionChanged"

View file

@ -28,5 +28,10 @@
"identifier": "cosmetic", "identifier": "cosmetic",
"i18nKey": "generatorconfig.categories.cosmetic", "i18nKey": "generatorconfig.categories.cosmetic",
"exclusive": false "exclusive": false
},
{
"identifier": "custom",
"i18nKey": "generatorconfig.categories.custom",
"exclusive": false
} }
] ]

View file

@ -8,7 +8,21 @@
"GMSJ0A": "GMSJ01 (NTSC-J 1.1)", "GMSJ0A": "GMSJ01 (NTSC-J 1.1)",
"GMSP01": "GMSP01 (PAL)", "GMSP01": "GMSP01 (PAL)",
"loadpresetplaceholder": "Lade eine Vorlage..", "loadpresetplaceholder": "Lade eine Vorlage..",
"selectionreset": "Deine Auswahl wird zurückgesetzt. Fortfahren?" "selectionreset": "Deine Auswahl wird zurückgesetzt. Fortfahren?",
"discard": "Deine Änderungen werden zurückgesetzt, fortfahren?"
},
"codeeditor": {
"fields": {
"title": {
"label": "Titel",
"placeholder": "N/A"
},
"value": {
"label": "Gecko Code",
"placeholder": "Dein Gecko Code.."
}
},
"save": "Speichern"
}, },
"headers": { "headers": {
"help": "Hilfe", "help": "Hilfe",
@ -39,7 +53,8 @@
"timer": "Timer", "timer": "Timer",
"misc": "Misc", "misc": "Misc",
"memcardpatch": "Memory Card Patches", "memcardpatch": "Memory Card Patches",
"cosmetic": "Kosmetisch" "cosmetic": "Kosmetisch",
"custom": "Benutzerdefiniert"
}, },
"presets": { "presets": {
"standard": "Standard", "standard": "Standard",

View file

@ -8,7 +8,21 @@
"GMSJ0A": "GMSJ01 (NTSC-J 1.1)", "GMSJ0A": "GMSJ01 (NTSC-J 1.1)",
"GMSP01": "GMSP01 (PAL)", "GMSP01": "GMSP01 (PAL)",
"loadpresetplaceholder": "Load a preset..", "loadpresetplaceholder": "Load a preset..",
"selectionreset": "This will reset your selection, continue?" "selectionreset": "This will reset your selection, continue?",
"discard": "This will discard all your changes, continue?"
},
"codeeditor": {
"fields": {
"title": {
"label": "Title",
"placeholder": "N/A"
},
"value": {
"label": "Gecko Code",
"placeholder": "Your Gecko Code.."
}
},
"save": "Save"
}, },
"headers": { "headers": {
"codelist": "Available Codes", "codelist": "Available Codes",
@ -39,7 +53,8 @@
"timer": "Timers", "timer": "Timers",
"misc": "Misc", "misc": "Misc",
"memcardpatch": "Memory Card Patches", "memcardpatch": "Memory Card Patches",
"cosmetic": "Cosmetic" "cosmetic": "Cosmetic",
"custom": "Custom"
}, },
"presets": { "presets": {
"standard": "Standard", "standard": "Standard",

View file

@ -6,6 +6,7 @@
body body
min-height: 120vh; min-height: 120vh;
font-size: 15px;
body.fool body.fool
transform: rotateY(-180deg); transform: rotateY(-180deg);
@ -24,3 +25,6 @@ footer.page-edit
aside.sidebar aside.sidebar
z-index: 18; z-index: 18;
header
z-index: 9991 !important;