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:**
|
**Example response:**
|
||||||
```json
|
```json
|
||||||
{"version": "1.9.2"}
|
{"version": "1.9.3"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `GET /api/status`
|
### `GET /api/status`
|
||||||
|
|||||||
+75
-9
@@ -18,7 +18,7 @@
|
|||||||
<div class="max-w-7xl mx-auto px-6 py-2">
|
<div class="max-w-7xl mx-auto px-6 py-2">
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
<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">
|
<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>
|
<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>
|
<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>
|
<tbody id="cards-tbody"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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="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>
|
<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>
|
</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>
|
<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>
|
<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>
|
||||||
<div id="pysim-cf-list" class="text-xs font-mono text-gray-600 dark:text-slate-400"></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">
|
<input type="file" id="pysim-cf-file" accept=".json,application/json" class="hidden" onchange="pysimCustomImportFile(this)">
|
||||||
<button onclick="pysimCustomExport()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700">Export as JSON</button>
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
<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>
|
<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>
|
</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>
|
<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>
|
</div>
|
||||||
@@ -4291,9 +4299,22 @@ function cardsExport() {
|
|||||||
el.select();
|
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 {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
if (text == null) text = await navigator.clipboard.readText();
|
||||||
const imported = JSON.parse(text);
|
const imported = JSON.parse(text);
|
||||||
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
||||||
let added = 0;
|
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 =====
|
// ===== pysim sub-tabs =====
|
||||||
let pysimCmdHistory = [];
|
let pysimCmdHistory = [];
|
||||||
let pysimCmdHistIdx = -1;
|
let pysimCmdHistIdx = -1;
|
||||||
@@ -5706,9 +5746,13 @@ function pysimCustomExport() {
|
|||||||
el.select();
|
el.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pysimCustomImport() {
|
function pysimCustomExportFile() {
|
||||||
|
downloadJson('otaman-custom-files.json', pysimCustomFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pysimCustomImport(text) {
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText();
|
if (text == null) text = await navigator.clipboard.readText();
|
||||||
const imported = JSON.parse(text);
|
const imported = JSON.parse(text);
|
||||||
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
if (!Array.isArray(imported)) throw new Error('Expected an array');
|
||||||
let added = 0;
|
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
|
// Init custom files
|
||||||
pysimCustomLoad();
|
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 — под списком карт.',
|
'Card presets are stored locally in your browser. JSON export/import buttons are below the card list.': 'Данные карт хранятся локально в браузере. Кнопки экспорта/импорта JSON — под списком карт.',
|
||||||
'Add': 'Добавить',
|
'Add': 'Добавить',
|
||||||
'Export as JSON': 'Экспорт в JSON',
|
'Export as JSON': 'Экспорт в JSON',
|
||||||
|
'Export to file': 'Экспорт в файл',
|
||||||
|
'Import from file': 'Импорт из файла',
|
||||||
|
'Paste & import': 'Вставить и импортировать',
|
||||||
'Import JSON from clipboard': 'Импорт JSON из буфера',
|
'Import JSON from clipboard': 'Импорт JSON из буфера',
|
||||||
'Name': 'Имя',
|
'Name': 'Имя',
|
||||||
'ICCID': 'ICCID',
|
'ICCID': 'ICCID',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'otaman-v13';
|
const CACHE = 'otaman-v14';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-otaman-server"
|
name = "pysim-otaman-server"
|
||||||
version = "1.9.2"
|
version = "1.9.3"
|
||||||
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# 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
|
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
|
# Static file serving (the PWA lives in <repo>/frontend, served by this server
|
||||||
|
|||||||
Reference in New Issue
Block a user