Card presets + custom files: file & pasted JSON import, file export; v1.9.3
Both sections (card presets, custom files) gain two import paths and a file download alongside the existing clipboard import: - Import from file: hidden <input type=file accept=.json> + FileReader feeding the unchanged dedupe/merge logic (works on plain-HTTP LAN origins where navigator.clipboard is unavailable) - Paste & import: reuses the IO textarea; guard requires '[' prefix so status messages are never mis-imported; empty state reveals + focuses the field with a hint placeholder - Export to file: Blob download (otaman-cards.json / otaman-custom-files.json) via shared downloadJson() helper - Import functions take optional text arg; clipboard stays the fallback - data-l10n on all five buttons per section; LANG_RU entries for the three new labels Version 1.9.2 -> 1.9.3 everywhere; SW cache otaman-v13 -> otaman-v14
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ Returns server version for compatibility checking.
|
||||
|
||||
**Example response:**
|
||||
```json
|
||||
{"version": "1.9.2"}
|
||||
{"version": "1.9.3"}
|
||||
```
|
||||
|
||||
### `GET /api/status`
|
||||
|
||||
+75
-9
@@ -18,7 +18,7 @@
|
||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.2</span></h1>
|
||||
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v1.9.3</span></h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
|
||||
<a href="https://github.com/anttro/otaman" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
|
||||
@@ -921,8 +921,12 @@
|
||||
<tbody id="cards-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<button onclick="cardsExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export as JSON">Export as JSON</button>
|
||||
<button onclick="cardsExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
|
||||
<button onclick="document.getElementById('cards-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
|
||||
<button onclick="cardsImportField()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Paste & import">Paste & import</button>
|
||||
<button onclick="cardsImport()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import JSON from clipboard">Import JSON from clipboard</button>
|
||||
</div>
|
||||
<textarea id="cards-io" rows="4" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 hidden" placeholder="JSON data"></textarea>
|
||||
@@ -997,9 +1001,13 @@
|
||||
<button onclick="pysimCustomAdd()" class="px-3 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
|
||||
</div>
|
||||
<div id="pysim-cf-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></div>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button onclick="pysimCustomExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">Export as JSON</button>
|
||||
<button onclick="pysimCustomImport()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700">Import JSON from clipboard</button>
|
||||
<input type="file" id="pysim-cf-file" accept=".json,application/json" class="hidden" onchange="pysimCustomImportFile(this)">
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
<button onclick="pysimCustomExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export as JSON">Export as JSON</button>
|
||||
<button onclick="pysimCustomExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
|
||||
<button onclick="document.getElementById('pysim-cf-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
|
||||
<button onclick="pysimCustomImportField()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Paste & import">Paste & import</button>
|
||||
<button onclick="pysimCustomImport()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import JSON from clipboard">Import JSON from clipboard</button>
|
||||
</div>
|
||||
<textarea id="pysim-cf-io" class="hidden mt-2 w-full font-mono text-xs border border-gray-300 dark:border-slate-600 rounded px-2 py-1.5 bg-gray-100 dark:bg-slate-800" rows="6"></textarea>
|
||||
</div>
|
||||
@@ -4291,9 +4299,22 @@ function cardsExport() {
|
||||
el.select();
|
||||
}
|
||||
|
||||
async function cardsImport() {
|
||||
function downloadJson(name, obj) {
|
||||
const blob = new Blob([JSON.stringify(obj, null, 2)], {type: 'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
function cardsExportFile() {
|
||||
downloadJson('otaman-cards.json', cards);
|
||||
}
|
||||
|
||||
async function cardsImport(text) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text == null) text = await navigator.clipboard.readText();
|
||||
const imported = JSON.parse(text);
|
||||
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
||||
let added = 0;
|
||||
@@ -4324,6 +4345,25 @@ async function cardsImport() {
|
||||
}
|
||||
}
|
||||
|
||||
function cardsImportFile(input) {
|
||||
const file = input.files && input.files[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => cardsImport(reader.result);
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function cardsImportField() {
|
||||
const el = document.getElementById('cards-io');
|
||||
const val = el.value.trim();
|
||||
if (!el.classList.contains('hidden') && val.startsWith('[')) { cardsImport(val); return; }
|
||||
el.value = '';
|
||||
el.placeholder = 'Paste JSON array here, then press Paste & import again';
|
||||
el.classList.remove('hidden');
|
||||
el.focus();
|
||||
}
|
||||
|
||||
// ===== pysim sub-tabs =====
|
||||
let pysimCmdHistory = [];
|
||||
let pysimCmdHistIdx = -1;
|
||||
@@ -5706,9 +5746,13 @@ function pysimCustomExport() {
|
||||
el.select();
|
||||
}
|
||||
|
||||
async function pysimCustomImport() {
|
||||
function pysimCustomExportFile() {
|
||||
downloadJson('otaman-custom-files.json', pysimCustomFiles);
|
||||
}
|
||||
|
||||
async function pysimCustomImport(text) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text == null) text = await navigator.clipboard.readText();
|
||||
const imported = JSON.parse(text);
|
||||
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
||||
let added = 0;
|
||||
@@ -5736,6 +5780,25 @@ async function pysimCustomImport() {
|
||||
}
|
||||
}
|
||||
|
||||
function pysimCustomImportFile(input) {
|
||||
const file = input.files && input.files[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => pysimCustomImport(reader.result);
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
function pysimCustomImportField() {
|
||||
const el = document.getElementById('pysim-cf-io');
|
||||
const val = el.value.trim();
|
||||
if (!el.classList.contains('hidden') && val.startsWith('[')) { pysimCustomImport(val); return; }
|
||||
el.value = '';
|
||||
el.placeholder = 'Paste JSON array here, then press Paste & import again';
|
||||
el.classList.remove('hidden');
|
||||
el.focus();
|
||||
}
|
||||
|
||||
// Init custom files
|
||||
pysimCustomLoad();
|
||||
|
||||
@@ -5833,6 +5896,9 @@ const LANG_RU = {
|
||||
'Card presets are stored locally in your browser. JSON export/import buttons are below the card list.': 'Данные карт хранятся локально в браузере. Кнопки экспорта/импорта JSON — под списком карт.',
|
||||
'Add': 'Добавить',
|
||||
'Export as JSON': 'Экспорт в JSON',
|
||||
'Export to file': 'Экспорт в файл',
|
||||
'Import from file': 'Импорт из файла',
|
||||
'Paste & import': 'Вставить и импортировать',
|
||||
'Import JSON from clipboard': 'Импорт JSON из буфера',
|
||||
'Name': 'Имя',
|
||||
'ICCID': 'ICCID',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'otaman-v13';
|
||||
const CACHE = 'otaman-v14';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-otaman-server"
|
||||
version = "1.9.2"
|
||||
version = "1.9.3"
|
||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -18,7 +18,7 @@ from osmocom.construct import GsmOrUcs2Adapter
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
|
||||
VERSION = '1.9.2'
|
||||
VERSION = '1.9.3'
|
||||
|
||||
|
||||
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||
|
||||
Reference in New Issue
Block a user