diff --git a/badgepresetio.js b/badgepresetio.js
new file mode 100644
index 000000000..653b123d2
--- /dev/null
+++ b/badgepresetio.js
@@ -0,0 +1,383 @@
+const PRESET_FILE_EXT = '.badgepreset';
+const PRESET_FILE_ACCEPT = '.badgepreset,.json';
+const PRESET_FILE_MAX_BYTES = 64 * 1024;
+const PRESET_FILE_MAX_ROWS = 10; // max badge slot based on server
+const PRESET_FILE_MAX_COLS = 7;
+const BADGE_ID_PATTERN = /^[a-zA-Z0-9_ ]+$/;
+const SLOT_SET_CONCURRENCY = 8;
+
+function getPresetIoMessage(key, fallback) {
+ return i18next.t(`modal.badgePreset.io.${key}`, fallback);
+}
+
+function showPresetIoSuccess(message) {
+ if (typeof showToastMessage === 'function')
+ showToastMessage(getMassagedLabel(message, true), 'info', true);
+}
+
+function getCurrentGridDimensions(fallbackSlots) {
+ const rows = typeof badgeSlotRows === 'number' && badgeSlotRows > 0
+ ? badgeSlotRows
+ : (fallbackSlots?.length || 1);
+ const cols = typeof badgeSlotCols === 'number' && badgeSlotCols > 0
+ ? badgeSlotCols
+ : (fallbackSlots?.[0]?.length || 3);
+ return { rows, cols };
+}
+
+function validateBadgeSlots(badgeSlots, maxRows, maxCols) {
+ if (!Array.isArray(badgeSlots))
+ return false;
+
+ if (maxRows && badgeSlots.length > maxRows)
+ return false;
+
+ for (const row of badgeSlots) {
+ if (!Array.isArray(row))
+ return false;
+ if (maxCols && row.length > maxCols)
+ return false;
+
+ for (const badgeId of row) {
+ if (badgeId === null || badgeId === 'null')
+ continue;
+ if (typeof badgeId !== 'string')
+ return false;
+ if (!BADGE_ID_PATTERN.test(badgeId))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function parsePresetFile(rawText, maxRows, maxCols) {
+ const parsed = JSON.parse(rawText);
+
+ let badgeSlots;
+ if (Array.isArray(parsed))
+ badgeSlots = parsed;
+ else if (parsed && Array.isArray(parsed.badgeSlots))
+ badgeSlots = parsed.badgeSlots;
+ else
+ return null;
+
+ if (!validateBadgeSlots(badgeSlots, maxRows, maxCols))
+ return null;
+
+ return badgeSlots;
+}
+
+function hasNonNullBadgeOutsideGrid(badgeSlots, rows, cols) {
+ for (let r = 0; r < badgeSlots.length; r++) {
+ const row = badgeSlots[r];
+ if (!Array.isArray(row))
+ continue;
+ for (let c = 0; c < row.length; c++) {
+ if (r >= rows || c >= cols) {
+ const badgeId = row[c];
+ if (badgeId != null && badgeId !== 'null')
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function expandBadgeSlotsToGrid(badgeSlots, rows, cols) {
+ const expanded = [];
+ for (let r = 0; r < rows; r++) {
+ const expandedRow = [];
+ for (let c = 0; c < cols; c++) {
+ const badgeId = badgeSlots?.[r]?.[c];
+ if (badgeId == null || badgeId === 'null')
+ expandedRow.push('null');
+ else
+ expandedRow.push(badgeId);
+ }
+ expanded.push(expandedRow);
+ }
+ return expanded;
+}
+
+function computeChanges(targetSlots, currentSlots, rows, cols) {
+ const changes = [];
+ for (let r = 0; r < rows; r++) {
+ for (let c = 0; c < cols; c++) {
+ const targetId = targetSlots[r]?.[c] ?? 'null';
+ const currentId = currentSlots[r]?.[c] ?? 'null';
+ if (targetId !== currentId)
+ changes.push({ r, c, targetId, currentId });
+ }
+ }
+ return changes;
+}
+
+function fetchSlotSet(badgeId, row, col) {
+ const encodedBadgeId = encodeURIComponent(badgeId);
+ return apiFetch(`badge?command=slotSet&id=${encodedBadgeId}&row=${row}&col=${col}`);
+}
+
+async function clearChangedSlotsFromChanges(changes) {
+ const tasks = [];
+ for (const { r, c, currentId } of changes) {
+ if (currentId === 'null')
+ continue;
+
+ tasks.push(
+ fetchSlotSet('null', r + 1, c + 1)
+ .then(response => ({ ok: response.ok }))
+ .catch(() => ({ ok: false }))
+ );
+ }
+
+ return Promise.all(tasks);
+}
+
+async function placeNonNullSlotsConcurrent(changes, concurrency) {
+ const tasks = changes
+ .filter(change => change.targetId !== 'null')
+ .map(({ r, c, targetId }) => async () => {
+ try {
+ const response = await fetchSlotSet(targetId, r + 1, c + 1);
+ if (response.ok)
+ return { ok: true };
+ const message = await response.text();
+ return {
+ ok: message.includes('unknown badge')
+ || message.includes('specified badge is locked')
+ };
+ } catch {
+ return { ok: false };
+ }
+ });
+
+ const results = [];
+ for (let index = 0; index < tasks.length; index += concurrency) {
+ const batch = tasks.slice(index, index + concurrency).map(task => task());
+ results.push(...await Promise.all(batch));
+ }
+ return results;
+}
+
+async function rollbackSlots(backupSlots) {
+ if (!Array.isArray(backupSlots))
+ return;
+
+ const promises = [];
+ for (let r = 0; r < backupSlots.length; r++) {
+ for (let c = 0; c < backupSlots[r].length; c++) {
+ const badgeId = backupSlots[r]?.[c] || 'null';
+ promises.push(fetchSlotSet(badgeId, r + 1, c + 1).catch(() => null));
+ }
+ }
+ await Promise.all(promises);
+}
+
+async function getPresetData(presetId) {
+ const response = await apiFetch(`badge?command=presetGet&preset=${presetId}`);
+ if (!response.ok)
+ return null;
+ return response.json();
+}
+
+function formatExportFilename(presetIndex) {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = `${now.getMonth() + 1}`.padStart(2, '0');
+ const day = `${now.getDate()}`.padStart(2, '0');
+ const hour = `${now.getHours()}`.padStart(2, '0');
+ const minute = `${now.getMinutes()}`.padStart(2, '0');
+ const second = `${now.getSeconds()}`.padStart(2, '0');
+ const formattedDate = `${year}-${month}-${day}-${hour}h${minute}m${second}s`;
+ const presetNumber = `${(parseInt(presetIndex, 10) || 0) + 1}`.padStart(2, '0');
+ return `badge_preset_${presetNumber}-${formattedDate}${PRESET_FILE_EXT}`;
+}
+
+const PRESET_FILE_SAVE_TYPES = [{
+ description: 'Badge Preset',
+ accept: { 'application/json': ['.badgepreset', '.json'] }
+}];
+
+function downloadJSON(data, filename) {
+ const json = JSON.stringify(data, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const blobUrl = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = blobUrl;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(blobUrl);
+}
+
+async function handleExport() {
+ const presetSelection = document.getElementById('badgePresetSelection');
+ if (!presetSelection) {
+ alert(getPresetIoMessage('exportFailed', 'Export failed.'));
+ return;
+ }
+
+ try {
+ const presetId = presetSelection.value;
+ const filename = formatExportFilename(presetId);
+ let fileHandle;
+
+ if (typeof showSaveFilePicker === 'function') {
+ try {
+ fileHandle = await showSaveFilePicker({
+ suggestedName: filename,
+ types: PRESET_FILE_SAVE_TYPES
+ });
+ } catch (error) {
+ if (error instanceof DOMException && error.name === 'AbortError')
+ return;
+ throw error;
+ }
+ }
+
+ const presetSlots = await getPresetData(presetId);
+ if (!presetSlots) {
+ alert(getPresetIoMessage('exportFailed', 'Export failed.'));
+ return;
+ }
+
+ if (isEmptyBadgeSlots(presetSlots)) {
+ alert(getPresetIoMessage('empty', 'This preset is empty.'));
+ return;
+ }
+
+ const { rows, cols } = getCurrentGridDimensions(presetSlots);
+ const fullGridSlots = expandBadgeSlotsToGrid(presetSlots, rows, cols);
+ const exportData = { badgeSlots: fullGridSlots };
+ if (fileHandle) {
+ const writable = await fileHandle.createWritable();
+ await writable.write(JSON.stringify(exportData, null, 2));
+ await writable.close();
+ } else {
+ downloadJSON(exportData, filename);
+ }
+ showPresetIoSuccess(getPresetIoMessage('exportSuccess', 'Badge preset exported successfully.'));
+ } catch (error) {
+ console.error('Export failed:', error);
+ alert(getPresetIoMessage('exportFailed', 'Export failed.'));
+ }
+}
+
+async function applyPresetToSlot(badgeSlots) {
+ const presetModal = document.getElementById('badgePresetModal');
+ const presetSelection = document.getElementById('badgePresetSelection');
+
+ if (!presetSelection || typeof apiFetch !== 'function')
+ return false;
+
+ let backupSlots = null;
+ let changedServerSlots = false;
+ let success = false;
+
+ try {
+ if (presetModal && typeof addLoader === 'function')
+ addLoader(presetModal, true);
+
+ const backupResponse = await apiFetch('badge?command=slotList');
+ if (!backupResponse.ok)
+ return false;
+ backupSlots = await backupResponse.json();
+
+ const playerRows = backupSlots.length;
+ const playerCols = backupSlots[0]?.length || 0;
+ if (hasNonNullBadgeOutsideGrid(badgeSlots, playerRows, playerCols))
+ return false;
+
+ const normalizedSlots = expandBadgeSlotsToGrid(badgeSlots, playerRows, playerCols);
+ const changes = computeChanges(normalizedSlots, backupSlots, playerRows, playerCols);
+ const clearResults = await clearChangedSlotsFromChanges(changes);
+ // if invalid badge id, skip place
+ const placeResults = await placeNonNullSlotsConcurrent(changes, SLOT_SET_CONCURRENCY);
+ changedServerSlots = true;
+
+ const allResults = [...clearResults, ...placeResults];
+ if (allResults.some(result => !result.ok))
+ return false;
+
+ const saveResponse = await apiFetch(`badge?command=presetSave&preset=${presetSelection.value}`);
+ if (!saveResponse.ok)
+ return false;
+
+ if (typeof initBadgePresetModal === 'function')
+ initBadgePresetModal();
+ success = true;
+ return true;
+ } finally {
+ try {
+ if (changedServerSlots && backupSlots)
+ await rollbackSlots(backupSlots);
+ } catch (error) {
+ console.error('Badge preset import rollback failed:', error);
+ } finally {
+ if (presetModal && typeof removeLoader === 'function')
+ removeLoader(presetModal);
+ }
+ if (success)
+ showPresetIoSuccess(getPresetIoMessage('importSuccess', 'Badge preset imported successfully.'));
+ }
+}
+
+function handleImport() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = PRESET_FILE_ACCEPT;
+ input.onchange = (event) => {
+ const file = event.target.files[0];
+ if (!file)
+ return;
+
+ if (file.size > PRESET_FILE_MAX_BYTES) {
+ alert(getPresetIoMessage('invalidFile', 'Invalid preset file.'));
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = async (loadEvent) => {
+ try {
+ const badgeSlots = parsePresetFile(loadEvent.target.result, PRESET_FILE_MAX_ROWS, PRESET_FILE_MAX_COLS);
+
+ if (!badgeSlots) {
+ alert(getPresetIoMessage('invalidFile', 'Invalid preset file.'));
+ return;
+ }
+
+ if (isEmptyBadgeSlots(badgeSlots)) {
+ alert(getPresetIoMessage('empty', 'This preset is empty.'));
+ return;
+ }
+
+ const imported = await applyPresetToSlot(badgeSlots);
+ if (!imported)
+ alert(getPresetIoMessage('importFailed', 'Import failed.'));
+ } catch (error) {
+ console.error('Import failed:', error);
+ alert(getPresetIoMessage('importFailed', 'Import failed.'));
+ }
+ };
+ reader.onerror = () => {
+ alert(getPresetIoMessage('invalidFile', 'Invalid preset file.'));
+ };
+ reader.readAsText(file);
+ };
+ input.click();
+}
+
+function initBadgePresetIO() {
+ const exportButton = document.getElementById('badgePresetExport');
+ const importButton = document.getElementById('badgePresetImport');
+ if (!exportButton || !importButton)
+ return;
+ if (exportButton.dataset.initialized === 'true')
+ return;
+
+ exportButton.onclick = handleExport;
+ importButton.onclick = handleImport;
+ exportButton.dataset.initialized = 'true';
+}
diff --git a/badges.js b/badges.js
index c565b0f09..63325c0c6 100644
--- a/badges.js
+++ b/badges.js
@@ -730,6 +730,8 @@ function initBadgeControls() {
};
document.getElementById('badgePresetSelection').onchange = initBadgePresetModal;
+ if (typeof initBadgePresetIO === 'function')
+ initBadgePresetIO();
document.getElementById('badgePresetSave').onclick = () => {
apiFetch(`badge?command=presetSave&preset=${document.getElementById('badgePresetSelection').value}`)
diff --git a/index.php b/index.php
index 948977816..6bbc82099 100644
--- a/index.php
+++ b/index.php
@@ -1164,9 +1164,11 @@ class="joystickBase" />
-
diff --git a/init.js b/init.js
index 4b49d4de7..94e310a1e 100644
--- a/init.js
+++ b/init.js
@@ -82,7 +82,7 @@ let initBlocker = Promise.resolve();
async function injectScripts() {
const supportsSimd = await wasmFeatureDetect.simd();
- let scripts = [ 'chat.js', 'playerlist.js', 'friends.js', 'parties.js', 'system.js', 'preloads.js', 'locations.js', 'schedules.js', 'report.js', 'notifications.js', '2kki.js', 'play.js', 'gamecanvas.js', `ynoengine${supportsSimd ? '-simd' : ''}.js` ];
+ let scripts = [ 'chat.js', 'playerlist.js', 'friends.js', 'parties.js', 'system.js', 'preloads.js', 'locations.js', 'schedules.js', 'report.js', 'notifications.js', '2kki.js', 'badgepresetio.js', 'play.js', 'gamecanvas.js', `ynoengine${supportsSimd ? '-simd' : ''}.js` ];
dependencyFiles['play.css'] = null;
diff --git a/lang/ar.json b/lang/ar.json
index e4f65bae7..e3d1c6210 100644
--- a/lang/ar.json
+++ b/lang/ar.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "إدارة حفظ البيانات",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/de.json b/lang/de.json
index 19b8e8484..df4697105 100644
--- a/lang/de.json
+++ b/lang/de.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Abzeichen Presets verwalten",
"selectPreset": "Presets auswählen",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Save verwalten",
diff --git a/lang/en.json b/lang/en.json
index 720eed6e8..d07a57c0a 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Manage Save Data",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/eo.json b/lang/eo.json
index 1ab475857..0078be300 100644
--- a/lang/eo.json
+++ b/lang/eo.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Administri Konservdatumojn",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/es.json b/lang/es.json
index 4153a56ae..7b034bb1d 100644
--- a/lang/es.json
+++ b/lang/es.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Administrar Preajustes de Insignias",
"selectPreset": "Seleccionar Preajuste",
- "presetName": "Preajuste {{index}}"
+ "presetName": "Preajuste {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Gestionar Datos de Guardado",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/fr.json b/lang/fr.json
index cf2b26331..6622355f7 100644
--- a/lang/fr.json
+++ b/lang/fr.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Gérer les préréglages de badges",
"selectPreset": "Choisir un préréglage",
- "presetName": "Préréglage {{index}}"
+ "presetName": "Préréglage {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Gérer les Données de Sauvegarde",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/id.json b/lang/id.json
index de53f8d66..ffe3cded9 100644
--- a/lang/id.json
+++ b/lang/id.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Manage Save Data",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/it.json b/lang/it.json
index 352efb383..ed3e20301 100644
--- a/lang/it.json
+++ b/lang/it.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Gestisci Dati di Salvataggio",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/ja.json b/lang/ja.json
index 10baa8377..4d6167f85 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "バッジプリセット",
"selectPreset": "プリセット選択",
- "presetName": "プリセット{{index}}"
+ "presetName": "プリセット{{index}}",
+ "export": "書き出し",
+ "import": "読み込み",
+ "io": {
+ "exportSuccess": "バッジプリセットの書き出しに成功しました。",
+ "importSuccess": "バッジプリセットの読み込みに成功しました。",
+ "empty": "このプリセットは空です。",
+ "invalidFile": "無効なプリセットファイルです。",
+ "exportFailed": "バッジプリセットの書き出しに失敗しました。",
+ "importFailed": "バッジプリセットの読み込みに失敗しました。"
+ }
},
"save": {
"title": "セーブデータ管理",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/ko.json b/lang/ko.json
index 096f753dd..f9a6c38c9 100644
--- a/lang/ko.json
+++ b/lang/ko.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "배지 프리셋 설정",
"selectPreset": "프리셋 선택",
- "presetName": "프리셋 {{index}}"
+ "presetName": "프리셋 {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "세이브 데이터 관리",
@@ -581,7 +591,8 @@
"subtitle": "일부 게임에는 다음과 같은 내용이 포함되어 있을 수 있습니다. 게임 플레이 시 주의하시길 바랍니다.",
"warning1": "광과민성 환자에게 영향을 끼칠 수 있는 번쩍거리는 빛의 연속적이거나 규칙적인 깜빡임",
"warning2": "자해 혹은 자살에 관련된 언급",
- "warning3": "불편하거나 충격적인 이미지"
+ "warning3": "불편하거나 충격적인 이미지",
+ "warning4": "Depictions of drug use."
},
"explorerUndiscoveredLocations": {
"title": "미발견 장소",
@@ -1237,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/pl.json b/lang/pl.json
index 20c29b9a3..3ab3eb4a8 100644
--- a/lang/pl.json
+++ b/lang/pl.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Zarządzaj zapisami",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/pt.json b/lang/pt.json
index cd52b5499..71de1c8ad 100644
--- a/lang/pt.json
+++ b/lang/pt.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Gerenciar Predefinições de Emblema",
"selectPreset": "Selecionar Predefinição",
- "presetName": "Predefinição {{index}}"
+ "presetName": "Predefinição {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Gerenciar Dados Salvos",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/ro.json b/lang/ro.json
index 8a84b027b..678e400f8 100644
--- a/lang/ro.json
+++ b/lang/ro.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Gestionare Presetări pt. Insigne",
"selectPreset": "Selectează Presetarea",
- "presetName": "Presetare {{index}}"
+ "presetName": "Presetare {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Gestionează Datele Salvate",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/ru.json b/lang/ru.json
index e194b681c..be28ded98 100644
--- a/lang/ru.json
+++ b/lang/ru.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Manage Badge Presets",
"selectPreset": "Select Preset",
- "presetName": "Preset {{index}}"
+ "presetName": "Preset {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Управление данными сохранений",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/tr.json b/lang/tr.json
index ee1fc37f0..254e6bd33 100644
--- a/lang/tr.json
+++ b/lang/tr.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Rozet Öntasarımlarını Ayarla",
"selectPreset": "Öntasarım Seç",
- "presetName": "Öntasarı {{index}}"
+ "presetName": "Öntasarı {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Kayıt Verisini Yönet",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/uk.json b/lang/uk.json
index e26f2bc34..a323f49d3 100644
--- a/lang/uk.json
+++ b/lang/uk.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Керування колекціями",
"selectPreset": "Обрати колекцію",
- "presetName": "Колекція {{index}}"
+ "presetName": "Колекція {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Керування даними збереження",
@@ -893,12 +903,12 @@
"markdownSupport": "Чат підтримує форматування за допомогою спеціальних символів. Доступні: **жирний** (\\*\\*текст\\*\\*), *курсив* (\\*текст\\*, \\_текст\\_), __підкреслений__ (\\_\\_текст\\_\\_), ~~закреслений~~ (\\~\\~текст\\~\\~) і ||спойлер|| (\\|\\|текст\\|\\|).",
"tabToChat": "На ПК, клавішою TAB можна перемикатися між грою та чатом. Цю функцію можна вимкнути у налаштуваннях.",
"chatTabNotifications": "Вкладинка чату підсвічується при нових повідомленнях, якщо ви знаходитесь у будь якій з них окрім «Усі».",
- "clearChat": "У правому верхньому кутку чату є кнопка Очистити. Нею можна видалити повідомлення з обраної вкладинки чату. Якщо натистуни її в «Усі» - вона очистить кожну вкладинку.",
+ "clearChat": "У правому верхньому кутку чату є кнопка Очистити. Нею можна видалити повідомлення з обраної вкладинки чату. Якщо натистуни її в «Усі» - вона очистить кожну вкладинку.",
"chatHistoryLimit": "На слабких пристроях гра може лагати від довгої історії чату. Кількість повідомлень можна обмежити у налаштуваннях.",
"parties": "Групи дуже зручні для подорожей разом. Ви можете бачити місцезнаходження учасників групи та спілкуватися з ними у окремому чаті.",
"immersionMode": "Якщо вам подобається більш особистий ігровий процес, а не соціальний, спробуйте режим занурення. Цей режим вимикає глобальний чат, список гравців та їхню кількість, що робить випадкові зустрічі цікавішими.",
"friendsMenu": "Права кнопка миші (або довгий дотик на мобільному пристрої) на гравці у списку, відчинить віконце де можна надіслати запит у друзі, згадати гравця або заблокувати його.",
- "easySettingsMenu": "На ПК можна натистуни клавішу F1, вона відчиняє меню важливих налаштувань (зміна керування, якість зображення гри, тощо).",
+ "easySettingsMenu": "На ПК можна натистуни клавішу F1, вона відчиняє меню важливих налаштувань (зміна керування, якість зображення гри, тощо).",
"floatingControls": "Якщо ви граєте на сенсорному пристрої і вам не подобається керування, спробуйте плаваючі типи елементів керування. Їх можна знайти у налаштуваннях мобільного керування.",
"recentBadges": "Випадково закрили сповіщення про отриманий значок? Його можна подивитись у меню значків обравши «Знайдені нещодавно».",
"desktopHotkeys": "На ПК, клавіші Alt+Enter перемикають повноекранний режим, F7 робить знімок екрану, англійська T відкриває чат.",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/vi.json b/lang/vi.json
index 67e3bc846..f300ecb12 100644
--- a/lang/vi.json
+++ b/lang/vi.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "Khuôn mẫu huy hiệu",
"selectPreset": "Chọn khuôn mẫu",
- "presetName": "Mẫu {{index}}"
+ "presetName": "Mẫu {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "Quản lý Dữ liệu lưu",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/lang/zh.json b/lang/zh.json
index 66e36105a..33d09bf98 100644
--- a/lang/zh.json
+++ b/lang/zh.json
@@ -420,7 +420,17 @@
"badgePreset": {
"title": "管理徽章预设",
"selectPreset": "选择预设",
- "presetName": "预设 {{index}}"
+ "presetName": "预设 {{index}}",
+ "export": "Export",
+ "import": "Import",
+ "io": {
+ "exportSuccess": "Badge preset exported successfully.",
+ "importSuccess": "Badge preset imported successfully.",
+ "empty": "This preset is empty.",
+ "invalidFile": "Invalid preset file.",
+ "exportFailed": "Export failed.",
+ "importFailed": "Import failed."
+ }
},
"save": {
"title": "存档",
@@ -1238,4 +1248,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/play.css b/play.css
index 3056079cf..97eb07aa0 100644
--- a/play.css
+++ b/play.css
@@ -4161,5 +4161,8 @@ html[dir="rtl"] .messageSender {
}
.badgePresetActions {
- display: flex
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 8px;
}