diff --git a/static/app.js b/static/app.js index 0efa0de..9c5149d 100644 --- a/static/app.js +++ b/static/app.js @@ -67,6 +67,8 @@ var cpsRepeaterTags = document.getElementById("cps-repeater-tags"); var cpsAddTgInput = document.getElementById("cps-add-tg-input"); var cpsAcList = document.getElementById("cps-ac-list"); + var cpsAliasFormat = document.getElementById("cps-alias-format"); + var cpsContactsBtn = document.getElementById("cps-contacts-btn"); var cpsDownloadBtn = document.getElementById("cps-download-btn"); var cpsCopyBtn = document.getElementById("cps-copy-btn"); var cpsChannelCount = document.getElementById("cps-channel-count"); @@ -325,7 +327,7 @@ var cc = r.color_code; talkgroups.forEach(function (tg) { var slot = tg.slot === "1" ? "SLOT1" : "SLOT2"; - var alias = (r.callsign + " TG" + tg.id).substring(0, 16); + var alias = tg.name.substring(0, 16); channels += buildChannel(alias, slot, cc, txHz, rxHz); }); }); @@ -397,6 +399,8 @@ function updateCpsButtons() { var hasData = getActiveRepeaters().length > 0 && cpsTalkgroups.length > 0; + var hasTgs = cpsTalkgroups.length > 0; + cpsContactsBtn.disabled = !hasTgs; cpsDownloadBtn.disabled = !hasData; cpsCopyBtn.disabled = !hasData; } @@ -408,6 +412,15 @@ cpsAcList.innerHTML = ""; } + function setSlot(idx, slot) { + cpsTalkgroups[idx].slot = slot; + if (cpsAliasFormat.value === "tg-ts") { + cpsTalkgroups[idx].name = formatTgAlias(cpsTalkgroups[idx].id, slot); + } + renderTgTable(); + updateChannelCount(); + } + function renderTgTable() { cpsTgTbody.innerHTML = ""; cpsTalkgroups.forEach(function (tg, idx) { @@ -420,7 +433,19 @@ var tdName = document.createElement("td"); tdName.className = "tg-name"; + tdName.contentEditable = "true"; + tdName.spellcheck = false; tdName.textContent = tg.name || t("cps_unknown_tg"); + tdName.addEventListener("blur", (function (i) { + return function () { + var val = tdName.textContent.trim().substring(0, 16); + cpsTalkgroups[i].name = val; + tdName.textContent = val || t("cps_unknown_tg"); + }; + })(idx)); + tdName.addEventListener("keydown", function (e) { + if (e.key === "Enter") { e.preventDefault(); tdName.blur(); } + }); tr.appendChild(tdName); var tdSlot = document.createElement("td"); @@ -435,10 +460,10 @@ btn2.textContent = "TS2"; if (tg.slot === "2") btn2.className = "active"; btn1.addEventListener("click", (function (i) { - return function () { cpsTalkgroups[i].slot = "1"; renderTgTable(); updateChannelCount(); }; + return function () { setSlot(i, "1"); }; })(idx)); btn2.addEventListener("click", (function (i) { - return function () { cpsTalkgroups[i].slot = "2"; renderTgTable(); updateChannelCount(); }; + return function () { setSlot(i, "2"); }; })(idx)); toggle.appendChild(btn1); toggle.appendChild(btn2); @@ -470,15 +495,74 @@ }); } + function formatTgAlias(tgId, slot) { + var fmt = cpsAliasFormat.value; + if (fmt === "tg-ts") { + return ("TG" + tgId + "-TS" + slot).substring(0, 16); + } + return ("TG" + tgId + " " + (tgName(tgId) || "")).trim().substring(0, 16); + } + + function reformatAllAliases() { + cpsTalkgroups.forEach(function (tg) { + tg.name = formatTgAlias(tg.id, tg.slot); + }); + renderTgTable(); + } + + function defaultSlot(tgId) { + var first = String(tgId).charAt(0); + return (first === "8" || first === "9") ? "2" : "1"; + } + function addTalkgroupById(tgId) { if (cpsTalkgroups.some(function (tg) { return tg.id === tgId; })) return; - cpsTalkgroups.push({ id: tgId, name: tgName(tgId), slot: "2" }); + var slot = defaultSlot(tgId); + cpsTalkgroups.push({ id: tgId, name: formatTgAlias(tgId, slot), slot: slot }); cpsTalkgroups.sort(function (a, b) { return a.id - b.id; }); renderTgTable(); updateChannelCount(); updateCpsButtons(); } + var CPS_CSV_HEADER = "ContactName,Delete_Contact,Rename_Contact,Comments," + + "Delete_FiveToneCalls,FiveToneCalls-S5CLDLL_5TTELEGRAM,FiveToneCalls-S5CLDLL_5TCALLADD," + + "Delete_MDCCalls,MDCCalls-AU_CALLLSTID,MDCCalls-AU_MDCSYS,MDCCalls-AU_RVRTPERS_Zone,MDCCalls-AU_RVRTPERS,MDCCalls-AU_SPTPLDPL,MDCCalls-AU_CALLTYPE," + + "Delete_QuikCallIICalls,QuikCallIICalls-QU_QCIISYS,QuikCallIICalls-QU_RVRTPERS_Zone,QuikCallIICalls-QU_RVRTPERS,QuikCallIICalls-QU_CALLFORMAT,QuikCallIICalls-QU_TONEATXFRE,QuikCallIICalls-QU_CODEA,QuikCallIICalls-QU_TONEBTXFRE,QuikCallIICalls-QU_CODEB,QuikCallIICalls-QU_STRIPPLDPL," + + "Delete_DigitalCalls,DigitalCalls-DU_CALLLSTID,DigitalCalls-DU_ROUTETYPE,DigitalCalls-DU_CALLPRCDTNEN,DigitalCalls-DU_RINGTYPE,DigitalCalls-DU_TXTMSGALTTNTP,DigitalCalls-DU_CALLTYPE,DigitalCalls-DU_OVCMCALL," + + "Delete_CapacityPlusCalls,CapacityPlusCalls-CAPPLUSUCL_CALLLSTID,CapacityPlusCalls-CAPPLUSUCL_ROUTETYPE,CapacityPlusCalls-CAPPLUSUCL_CALLPRCDTNEN,CapacityPlusCalls-CAPPLUSUCL_RINGTYPE,CapacityPlusCalls-CAPPLUSUCL_TXTMSGALTTNTP,CapacityPlusCalls-CAPPLUSUCL_CALLTYPE," + + "Delete_PhoneCalls,PhoneCalls-PHNUCLELL_CALLID,PhoneCalls-PHNUCLELL_RINGTYPE"; + + function generateContactsCsv(talkgroups) { + var rows = [CPS_CSV_HEADER]; + talkgroups.forEach(function (tg) { + var name = ("TG" + tg.id + " " + (tg.name || "")).trim().substring(0, 16); + var row = name + ",False,,," + + "False,,," + + "False,,,,,," + + "False,,,,,,,,," + + "False," + tg.id + ",Regular,False,No Style,Repetitive,Group Call,False," + + "False,,Regular,False,No Style,Repetitive,Group Call," + + "False,,No Style"; + rows.push(row); + }); + return rows.join("\r\n"); + } + + function downloadContactsCsv() { + if (!cpsTalkgroups.length) return; + var csv = generateContactsCsv(cpsTalkgroups); + var blob = new Blob([csv], { type: "text/csv" }); + var url = URL.createObjectURL(blob); + var a = document.createElement("a"); + a.href = url; + a.download = "dmrmap-contacts.csv"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + function downloadCpsXml() { var active = getActiveRepeaters(); if (!cpsTalkgroups.length || !active.length) return; @@ -1277,6 +1361,8 @@ } } }); + cpsAliasFormat.addEventListener("change", reformatAllAliases); + cpsContactsBtn.addEventListener("click", downloadContactsCsv); cpsDownloadBtn.addEventListener("click", downloadCpsXml); cpsCopyBtn.addEventListener("click", copyCpsXml); diff --git a/static/index.html b/static/index.html index 582da7f..478e308 100644 --- a/static/index.html +++ b/static/index.html @@ -87,6 +87,13 @@

This feature is still in development. Please review the generated output before importing.

+
+ + +
@@ -106,8 +113,19 @@ diff --git a/static/locales/de.json b/static/locales/de.json index f2e0fc6..64e3053 100644 --- a/static/locales/de.json +++ b/static/locales/de.json @@ -46,7 +46,11 @@ "cps_tg_name": "Name", "cps_tg_slot": "Zeitschlitz", "cps_add_tg_placeholder": "TG per ID oder Name hinzufügen...", - "cps_download_xml": "XML herunterladen", + "cps_alias_format": "Kanal-Alias-Format", + "cps_step1_label": "Schritt 1: Kontakte importieren", + "cps_download_contacts": "Kontakte CSV herunterladen", + "cps_step2_label": "Schritt 2: Zone einfügen", + "cps_download_xml": "Zone XML herunterladen", "cps_copy_xml": "In Zwischenablage", "cps_copied": "Kopiert!", "cps_unknown_tg": "Unbekannt", diff --git a/static/locales/en.json b/static/locales/en.json index 35a724e..b198fd0 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -46,7 +46,11 @@ "cps_tg_name": "Name", "cps_tg_slot": "Timeslot", "cps_add_tg_placeholder": "Add TG by ID or name...", - "cps_download_xml": "Download XML", + "cps_alias_format": "Channel alias format", + "cps_step1_label": "Step 1: Import contacts", + "cps_download_contacts": "Download Contacts CSV", + "cps_step2_label": "Step 2: Paste zone", + "cps_download_xml": "Download Zone XML", "cps_copy_xml": "Copy to Clipboard", "cps_copied": "Copied!", "cps_unknown_tg": "Unknown", diff --git a/static/locales/es.json b/static/locales/es.json index b735105..dccd6db 100644 --- a/static/locales/es.json +++ b/static/locales/es.json @@ -46,7 +46,11 @@ "cps_tg_name": "Nombre", "cps_tg_slot": "Timeslot", "cps_add_tg_placeholder": "Añadir TG por ID o nombre...", - "cps_download_xml": "Descargar XML", + "cps_alias_format": "Formato de alias del canal", + "cps_step1_label": "Paso 1: Importar contactos", + "cps_download_contacts": "Descargar contactos CSV", + "cps_step2_label": "Paso 2: Pegar zona", + "cps_download_xml": "Descargar zona XML", "cps_copy_xml": "Copiar al portapapeles", "cps_copied": "¡Copiado!", "cps_unknown_tg": "Desconocido", diff --git a/static/locales/fr.json b/static/locales/fr.json index a24fcc7..6c1eb85 100644 --- a/static/locales/fr.json +++ b/static/locales/fr.json @@ -46,7 +46,11 @@ "cps_tg_name": "Nom", "cps_tg_slot": "Timeslot", "cps_add_tg_placeholder": "Ajouter TG par ID ou nom...", - "cps_download_xml": "Télécharger XML", + "cps_alias_format": "Format d'alias du canal", + "cps_step1_label": "Étape 1 : Importer les contacts", + "cps_download_contacts": "Télécharger contacts CSV", + "cps_step2_label": "Étape 2 : Coller la zone", + "cps_download_xml": "Télécharger zone XML", "cps_copy_xml": "Copier dans le presse-papiers", "cps_copied": "Copié !", "cps_unknown_tg": "Inconnu", diff --git a/static/locales/it.json b/static/locales/it.json index 99b63ca..e1e76ff 100644 --- a/static/locales/it.json +++ b/static/locales/it.json @@ -46,7 +46,11 @@ "cps_tg_name": "Nome", "cps_tg_slot": "Timeslot", "cps_add_tg_placeholder": "Aggiungi TG per ID o nome...", - "cps_download_xml": "Scarica XML", + "cps_alias_format": "Formato alias canale", + "cps_step1_label": "Passo 1: Importa contatti", + "cps_download_contacts": "Scarica contatti CSV", + "cps_step2_label": "Passo 2: Incolla zona", + "cps_download_xml": "Scarica zona XML", "cps_copy_xml": "Copia negli appunti", "cps_copied": "Copiato!", "cps_unknown_tg": "Sconosciuto", diff --git a/static/locales/pl.json b/static/locales/pl.json index c569497..d58c1c7 100644 --- a/static/locales/pl.json +++ b/static/locales/pl.json @@ -48,7 +48,11 @@ "cps_tg_name": "Nazwa", "cps_tg_slot": "Timeslot", "cps_add_tg_placeholder": "Dodaj TG po ID lub nazwie...", - "cps_download_xml": "Pobierz XML", + "cps_alias_format": "Format aliasu kanału", + "cps_step1_label": "Krok 1: Importuj kontakty", + "cps_download_contacts": "Pobierz kontakty CSV", + "cps_step2_label": "Krok 2: Wklej strefę", + "cps_download_xml": "Pobierz strefę XML", "cps_copy_xml": "Kopiuj do schowka", "cps_copied": "Skopiowano!", "cps_unknown_tg": "Nieznany", diff --git a/static/style.css b/static/style.css index 42ee629..5c26ac7 100644 --- a/static/style.css +++ b/static/style.css @@ -785,6 +785,48 @@ html, body { opacity: 1; } +.cps-alias-format { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.cps-alias-format label { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + white-space: nowrap; +} + +.cps-alias-format select { + flex: 1; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: 4px; + font-size: 11px; + background: var(--input-bg); + color: var(--text); + outline: none; +} + +.cps-tg-table .tg-name[contenteditable] { + cursor: text; + border-radius: 3px; + padding: 2px 4px; + margin: -2px -4px; + outline: none; +} + +.cps-tg-table .tg-name[contenteditable]:hover { + background: var(--hover-bg); +} + +.cps-tg-table .tg-name[contenteditable]:focus { + background: var(--input-bg); + box-shadow: 0 0 0 1px #4CAF50; +} + .cps-tg-table { width: 100%; border-collapse: collapse; @@ -902,11 +944,31 @@ html, body { .cps-modal-footer { display: flex; - gap: 8px; + flex-direction: column; + gap: 10px; padding: 12px 18px; border-top: 1px solid var(--border-light); } +.cps-export-step { + display: flex; + flex-direction: column; + gap: 6px; +} + +.cps-step-label { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.cps-step-buttons { + display: flex; + gap: 8px; +} + .cps-btn-primary { flex: 1; padding: 8px 16px;