mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-26 13:07:55 +00:00
feat(frontend): interfaces, archives, messages, settings, i18n
This commit is contained in:
@@ -55,6 +55,14 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="selectedArchives.length > 0"
|
||||
class="text-[10px] font-bold uppercase tracking-wider text-blue-500 hover:text-blue-600 transition-colors flex items-center gap-1 mr-2"
|
||||
@click="exportSelectedArchivesAsMu"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="download" class="size-3.5" />
|
||||
{{ $t("archives.export_selected_mu", { count: selectedArchives.length }) }}
|
||||
</button>
|
||||
<button
|
||||
v-if="selectedArchives.length > 0"
|
||||
class="text-[10px] font-bold uppercase tracking-wider text-red-500 hover:text-red-600 transition-colors flex items-center gap-1"
|
||||
@@ -157,6 +165,17 @@
|
||||
<MaterialDesignIcon icon-name="view-list" class="size-4" />
|
||||
</button>
|
||||
<div class="hidden sm:block w-px h-6 bg-zinc-800 mx-1"></div>
|
||||
<button
|
||||
class="p-2 hover:bg-zinc-800 rounded transition-colors text-zinc-300 flex items-center gap-2"
|
||||
:title="$t('archives.export_mu')"
|
||||
@click="exportArchiveAsMu(viewingArchive)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="download" class="size-4" />
|
||||
<span class="hidden xs:inline text-xs font-bold uppercase tracking-wider">{{
|
||||
$t("archives.export_mu")
|
||||
}}</span>
|
||||
</button>
|
||||
<div class="hidden xs:block w-px h-6 bg-zinc-800 mx-1"></div>
|
||||
<button
|
||||
class="p-2 hover:bg-zinc-800 rounded transition-colors text-blue-400 flex items-center gap-2"
|
||||
@click="openInNomadnet(viewingArchive)"
|
||||
@@ -239,8 +258,9 @@ export default {
|
||||
groupedArchives() {
|
||||
// Optimization: Use a simple object for grouping
|
||||
const groups = {};
|
||||
for (let i = 0; i < this.archives.length; i++) {
|
||||
const archive = this.archives[i];
|
||||
const list = this.archives || [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const archive = list[i];
|
||||
const hash = archive.destination_hash;
|
||||
if (!groups[hash]) {
|
||||
groups[hash] = {
|
||||
@@ -398,6 +418,50 @@ export default {
|
||||
formatDate(dateStr) {
|
||||
return Utils.formatTimeAgo(dateStr);
|
||||
},
|
||||
downloadTextAsFile(content, filename) {
|
||||
const blob = new Blob([content ?? ""], { type: "text/plain;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.rel = "noopener";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
muExportBasename(archive) {
|
||||
let base = (archive.page_path || "page").split("/").pop() || "page";
|
||||
base = base.replace(/[\\/:*?"<>|]+/g, "_").trim() || "page";
|
||||
return base;
|
||||
},
|
||||
muExportFilename(archive) {
|
||||
let base = this.muExportBasename(archive);
|
||||
if (base.toLowerCase().endsWith(".mu")) {
|
||||
return base;
|
||||
}
|
||||
const without = base.includes(".") ? base.replace(/\.[^.]+$/, "") : base;
|
||||
return `${without || "page"}.mu`;
|
||||
},
|
||||
muExportFilenameDisambiguated(archive) {
|
||||
const stem = this.muExportFilename(archive).replace(/\.mu$/i, "");
|
||||
const short = (archive.hash || "snap").substring(0, 8);
|
||||
return `${stem}_${short}.mu`;
|
||||
},
|
||||
exportArchiveAsMu(archive) {
|
||||
if (!archive) {
|
||||
return;
|
||||
}
|
||||
this.downloadTextAsFile(archive.content, this.muExportFilename(archive));
|
||||
},
|
||||
exportSelectedArchivesAsMu() {
|
||||
const list = this.archives.filter((a) => this.selectedArchives.includes(a.id));
|
||||
list.forEach((archive, i) => {
|
||||
window.setTimeout(() => {
|
||||
this.downloadTextAsFile(archive.content, this.muExportFilenameDisambiguated(archive));
|
||||
}, i * 120);
|
||||
});
|
||||
},
|
||||
renderFullContent(archive) {
|
||||
if (!archive.content) return "";
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1105,6 +1105,7 @@
|
||||
:placeholder="$t('messages.send_placeholder')"
|
||||
@keydown.enter.exact.prevent="onEnterPressed"
|
||||
@keydown.enter.shift.exact.prevent="onShiftEnterPressed"
|
||||
@paste="onMessagePaste"
|
||||
></textarea>
|
||||
|
||||
<!-- reply preview -->
|
||||
@@ -1265,6 +1266,15 @@
|
||||
<MaterialDesignIcon icon-name="refresh" class="size-4" />
|
||||
<span class="font-medium">Retry</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isSelectedPeerBlocked && selectedPeer"
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-emerald-600 dark:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 transition-all active:scale-95"
|
||||
@click="liftBanishmentFromMessageMenu"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-4" />
|
||||
<span class="font-medium">{{ $t("banishment.lift_banishment") }}</span>
|
||||
</button>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-700 my-1.5 mx-2"></div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2925,6 +2935,23 @@ export default {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
async liftBanishmentFromMessageMenu() {
|
||||
if (!this.selectedPeer?.destination_hash) {
|
||||
this.messageContextMenu.show = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await window.axios.delete(
|
||||
`/api/v1/blocked-destinations/${this.selectedPeer.destination_hash}`
|
||||
);
|
||||
GlobalEmitter.emit("block-status-changed");
|
||||
DialogUtils.alert(this.$t("banishment.banishment_lifted"));
|
||||
} catch (e) {
|
||||
DialogUtils.alert(this.$t("banishment.failed_lift_banishment"));
|
||||
console.error(e);
|
||||
}
|
||||
this.messageContextMenu.show = false;
|
||||
},
|
||||
onChatItemClick: function (chatItem) {
|
||||
if (!chatItem.is_actions_expanded) {
|
||||
chatItem.is_actions_expanded = true;
|
||||
@@ -2973,8 +3000,8 @@ export default {
|
||||
this.messageContextMenu.show = true;
|
||||
|
||||
this.$nextTick(() => {
|
||||
const menuWidth = 180;
|
||||
const menuHeight = 150;
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 280;
|
||||
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
@@ -3673,6 +3700,35 @@ export default {
|
||||
DialogUtils.alert(message);
|
||||
}
|
||||
},
|
||||
onMessagePaste(event) {
|
||||
const cd = event.clipboardData;
|
||||
if (!cd?.items?.length) {
|
||||
return;
|
||||
}
|
||||
const imageBlobs = [];
|
||||
for (let i = 0; i < cd.items.length; i++) {
|
||||
const item = cd.items[i];
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
const f = item.getAsFile();
|
||||
if (f) {
|
||||
imageBlobs.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imageBlobs.length === 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const t = Date.now();
|
||||
imageBlobs.forEach((file, idx) => {
|
||||
let f = file;
|
||||
const ext = (file.type.split("/")[1] || "png").replace("jpeg", "jpg");
|
||||
if (!file.name || file.name === "image.png" || file.name === "image.jpeg") {
|
||||
f = new File([file], `paste-${t}-${idx}.${ext}`, { type: file.type });
|
||||
}
|
||||
this.onImageSelected(f);
|
||||
});
|
||||
},
|
||||
async pasteFromClipboard() {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
|
||||
@@ -1078,31 +1078,25 @@
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $t("app.announce_limits") }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ $t("app.announce_limits_description") }}
|
||||
</div>
|
||||
<div class="text-xs font-semibold text-gray-700 dark:text-zinc-300 uppercase tracking-wide">
|
||||
{{ $t("app.announce_max_stored_heading") }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{ $t("app.announce_limit_lxmf") }}</label>
|
||||
<input
|
||||
v-model.number="config.announce_limit_lxmf_delivery"
|
||||
v-model.number="config.announce_max_stored_lxmf_delivery"
|
||||
type="number"
|
||||
min="0"
|
||||
min="1"
|
||||
class="input-field"
|
||||
placeholder="-"
|
||||
@change="
|
||||
updateConfig(
|
||||
{
|
||||
announce_limit_lxmf_delivery:
|
||||
config.announce_limit_lxmf_delivery ?? null,
|
||||
},
|
||||
'announce_limits'
|
||||
)
|
||||
"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
@@ -1110,42 +1104,91 @@
|
||||
$t("app.announce_limit_nomadnet")
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="config.announce_limit_nomadnetwork_node"
|
||||
v-model.number="config.announce_max_stored_nomadnetwork_node"
|
||||
type="number"
|
||||
min="0"
|
||||
min="1"
|
||||
class="input-field"
|
||||
placeholder="-"
|
||||
@change="
|
||||
updateConfig(
|
||||
{
|
||||
announce_limit_nomadnetwork_node:
|
||||
config.announce_limit_nomadnetwork_node ?? null,
|
||||
},
|
||||
'announce_limits'
|
||||
)
|
||||
"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{ $t("app.announce_limit_prop") }}</label>
|
||||
<input
|
||||
v-model.number="config.announce_limit_lxmf_propagation"
|
||||
v-model.number="config.announce_max_stored_lxmf_propagation"
|
||||
type="number"
|
||||
min="0"
|
||||
min="1"
|
||||
class="input-field"
|
||||
placeholder="-"
|
||||
@change="
|
||||
updateConfig(
|
||||
{
|
||||
announce_limit_lxmf_propagation:
|
||||
config.announce_limit_lxmf_propagation ?? null,
|
||||
},
|
||||
'announce_limits'
|
||||
)
|
||||
"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs font-semibold text-gray-700 dark:text-zinc-300 uppercase tracking-wide">
|
||||
{{ $t("app.announce_fetch_limit_heading") }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{ $t("app.announce_limit_lxmf") }}</label>
|
||||
<input
|
||||
v-model.number="config.announce_fetch_limit_lxmf_delivery"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{
|
||||
$t("app.announce_limit_nomadnet")
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="config.announce_fetch_limit_nomadnetwork_node"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{ $t("app.announce_limit_prop") }}</label>
|
||||
<input
|
||||
v-model.number="config.announce_fetch_limit_lxmf_propagation"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{ $t("app.announce_search_max_fetch") }}</label>
|
||||
<input
|
||||
v-model.number="config.announce_search_max_fetch"
|
||||
type="number"
|
||||
min="100"
|
||||
class="input-field"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
<p class="text-[10px] text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("app.announce_search_max_fetch_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-xs font-medium">{{
|
||||
$t("app.discovered_interfaces_max_return")
|
||||
}}</label>
|
||||
<input
|
||||
v-model.number="config.discovered_interfaces_max_return"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
@change="onAnnounceLimitsChange"
|
||||
/>
|
||||
<p class="text-[10px] text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("app.discovered_interfaces_max_return_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1496,7 +1539,9 @@
|
||||
@update:model-value="onInboundStampsEnabledChange"
|
||||
/>
|
||||
<span class="setting-toggle__label">
|
||||
<span class="setting-toggle__title">{{ $t("app.inbound_stamps_required_title") }}</span>
|
||||
<span class="setting-toggle__title">{{
|
||||
$t("app.inbound_stamps_required_title")
|
||||
}}</span>
|
||||
<span class="setting-toggle__description">{{
|
||||
$t("app.inbound_stamps_required_description")
|
||||
}}</span>
|
||||
@@ -1762,9 +1807,14 @@ export default {
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
blackhole_integration_enabled: true,
|
||||
announce_limit_lxmf_delivery: null,
|
||||
announce_limit_nomadnetwork_node: null,
|
||||
announce_limit_lxmf_propagation: null,
|
||||
announce_max_stored_lxmf_delivery: 1000,
|
||||
announce_max_stored_nomadnetwork_node: 1000,
|
||||
announce_max_stored_lxmf_propagation: 1000,
|
||||
announce_fetch_limit_lxmf_delivery: 500,
|
||||
announce_fetch_limit_nomadnetwork_node: 500,
|
||||
announce_fetch_limit_lxmf_propagation: 500,
|
||||
announce_search_max_fetch: 2000,
|
||||
discovered_interfaces_max_return: 500,
|
||||
message_font_size: 14,
|
||||
message_icon_size: 28,
|
||||
message_outbound_bubble_color: "#4f46e5",
|
||||
@@ -1867,6 +1917,10 @@ export default {
|
||||
"app.announce_limit_lxmf",
|
||||
"app.announce_limit_nomadnet",
|
||||
"app.announce_limit_prop",
|
||||
"app.announce_max_stored_heading",
|
||||
"app.announce_fetch_limit_heading",
|
||||
"app.announce_search_max_fetch",
|
||||
"app.discovered_interfaces_max_return",
|
||||
],
|
||||
transport: [
|
||||
"Reticulum",
|
||||
@@ -2083,6 +2137,37 @@ export default {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
numOrNull(v) {
|
||||
if (v === null || v === undefined || v === "") {
|
||||
return null;
|
||||
}
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
},
|
||||
async onAnnounceLimitsChange() {
|
||||
const c = this.config;
|
||||
await this.updateConfig(
|
||||
{
|
||||
announce_max_stored_lxmf_delivery: this.numOrNull(c.announce_max_stored_lxmf_delivery),
|
||||
announce_max_stored_nomadnetwork_node: this.numOrNull(
|
||||
c.announce_max_stored_nomadnetwork_node
|
||||
),
|
||||
announce_max_stored_lxmf_propagation: this.numOrNull(
|
||||
c.announce_max_stored_lxmf_propagation
|
||||
),
|
||||
announce_fetch_limit_lxmf_delivery: this.numOrNull(c.announce_fetch_limit_lxmf_delivery),
|
||||
announce_fetch_limit_nomadnetwork_node: this.numOrNull(
|
||||
c.announce_fetch_limit_nomadnetwork_node
|
||||
),
|
||||
announce_fetch_limit_lxmf_propagation: this.numOrNull(
|
||||
c.announce_fetch_limit_lxmf_propagation
|
||||
),
|
||||
announce_search_max_fetch: this.numOrNull(c.announce_search_max_fetch),
|
||||
discovered_interfaces_max_return: this.numOrNull(c.discovered_interfaces_max_return),
|
||||
},
|
||||
"announce_limits"
|
||||
);
|
||||
},
|
||||
async copyValue(value, label) {
|
||||
if (!value) {
|
||||
ToastUtils.warning(`Nothing to copy for ${label}`);
|
||||
@@ -2265,10 +2350,7 @@ export default {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const restore = Math.min(
|
||||
254,
|
||||
Math.max(1, Number(this.lastRememberedInboundStampCost) || 8)
|
||||
);
|
||||
const restore = Math.min(254, Math.max(1, Number(this.lastRememberedInboundStampCost) || 8));
|
||||
this.config.lxmf_inbound_stamp_cost = restore;
|
||||
await this.updateConfig(
|
||||
{
|
||||
|
||||
@@ -207,7 +207,13 @@
|
||||
"blackhole_integration_enabled": "Blackhole-Integration",
|
||||
"blackhole_integration_description": "Identitäten automatisch auf der Reticulum-Transportebene sperren (Blackhole), wenn Benutzer in MeshChatX verbannt werden.",
|
||||
"announce_limits": "Ankündigungslimits",
|
||||
"announce_limits_description": "Optionale maximale Anzahl pro Aspekt. Leer lassen für kein Limit. Schützt vor Flood-Angriffen in öffentlichen Netzen.",
|
||||
"announce_limits_description": "Begrenzt gespeicherte Ankündigungen pro Aspekt (älteste werden entfernt) und wie viele pro API-Anfrage geladen werden. Begrenzt auch Suche und erkannte Schnittstellen. Feld leeren und speichern setzt Standardwerte.",
|
||||
"announce_max_stored_heading": "Max. gespeichert pro Aspekt (Datenbank)",
|
||||
"announce_fetch_limit_heading": "Standard-Seitengröße (API)",
|
||||
"announce_search_max_fetch": "Max. Ankündigungen für Suche",
|
||||
"announce_search_max_fetch_hint": "Maximale Zeilen aus der Datenbank bei Textsuche (pro Anfrage).",
|
||||
"discovered_interfaces_max_return": "Limit erkannte Schnittstellen",
|
||||
"discovered_interfaces_max_return_hint": "Maximale Einträge für erkannte und aktive Schnittstellen in der Oberfläche.",
|
||||
"announce_limit_lxmf": "LXMF",
|
||||
"announce_limit_nomadnet": "NomadNet",
|
||||
"announce_limit_prop": "Prop-Knoten",
|
||||
@@ -505,7 +511,20 @@
|
||||
"select_config_file": "Please select a configuration file",
|
||||
"select_at_least_one": "Please select at least one interface to import",
|
||||
"import_success": "Interfaces imported successfully. MeshChat must be restarted for these changes to take effect.",
|
||||
"failed_import_all": "Failed to import interfaces"
|
||||
"failed_import_all": "Failed to import interfaces",
|
||||
"add_interface_sidebar_a11y": "Verknüpfungen und Community-Voreinstellungen",
|
||||
"community_quick_start": "Community-Schnellstart",
|
||||
"community_quick_start_hint": "Voreinstellungen von directory.rns.recipes (Online-Listings).",
|
||||
"community_quick_start_hide": "Vorschläge ausblenden",
|
||||
"community_use_preset": "Übernehmen",
|
||||
"community_presets_hidden_hint": "Community-Voreinstellungen sind ausgeblendet. Hier oder in den Einstellungen wieder anzeigen.",
|
||||
"community_presets_show_again": "Community-Voreinstellungen anzeigen",
|
||||
"community_presets_empty": "Keine Voreinstellungen. Bundle neu bauen oder public/community_interfaces.json bereitstellen.",
|
||||
"find_more_nodes": "Weitere Knoten finden",
|
||||
"quick_import": "Schnellimport",
|
||||
"quick_import_paste_hint": "Rohkonfiguration einfügen",
|
||||
"quick_import_placeholder": "[[Interface Name]]\ntype = TCPClientInterface\ntarget_host = 1.2.3.4",
|
||||
"quick_import_apply": "Anwenden: {name}"
|
||||
},
|
||||
"map": {
|
||||
"title": "Karte",
|
||||
@@ -827,7 +846,9 @@
|
||||
"pages": "Seiten",
|
||||
"view": "Anzeigen",
|
||||
"showing_range": "Zeige {start} bis {end} von {total} Archiven",
|
||||
"page_of": "Seite {page} von {total_pages}"
|
||||
"page_of": "Seite {page} von {total_pages}",
|
||||
"export_mu": "Als .mu exportieren",
|
||||
"export_selected_mu": ".mu exportieren ({count})"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Dokumentation",
|
||||
|
||||
@@ -210,7 +210,13 @@
|
||||
"blackhole_integration_enabled": "Blackhole Integration",
|
||||
"blackhole_integration_description": "Automatically blackhole identities at the Reticulum transport layer when banishing users in MeshChatX.",
|
||||
"announce_limits": "Announce Limits",
|
||||
"announce_limits_description": "Optional max count per aspect. Leave empty for no limit. Protects against flood attacks on public networks.",
|
||||
"announce_limits_description": "Limit how many announces are kept in the database per aspect (oldest removed first) and how many load per API request. Also caps search and discovered-interface payloads. Clear a numeric field and save to restore defaults.",
|
||||
"announce_max_stored_heading": "Max stored per aspect (database)",
|
||||
"announce_fetch_limit_heading": "Default page size (API)",
|
||||
"announce_search_max_fetch": "Max announces for search",
|
||||
"announce_search_max_fetch_hint": "Maximum rows read from the database when you use text search on announces (per request).",
|
||||
"discovered_interfaces_max_return": "Discovered interfaces cap",
|
||||
"discovered_interfaces_max_return_hint": "Maximum discovered and active interface entries returned to the UI.",
|
||||
"announce_limit_lxmf": "LXMF",
|
||||
"announce_limit_nomadnet": "NomadNet",
|
||||
"announce_limit_prop": "Prop Nodes",
|
||||
@@ -459,7 +465,20 @@
|
||||
"select_config_file": "Please select a configuration file",
|
||||
"select_at_least_one": "Please select at least one interface to import",
|
||||
"import_success": "Interfaces imported successfully. MeshChat must be restarted for these changes to take effect.",
|
||||
"failed_import_all": "Failed to import interfaces"
|
||||
"failed_import_all": "Failed to import interfaces",
|
||||
"add_interface_sidebar_a11y": "Shortcuts and community presets",
|
||||
"community_quick_start": "Community Quick-Start",
|
||||
"community_quick_start_hint": "Presets from directory.rns.recipes online listings.",
|
||||
"community_quick_start_hide": "Hide suggested interfaces",
|
||||
"community_use_preset": "Use This",
|
||||
"community_presets_hidden_hint": "Community presets are hidden. Show them again here or in Settings.",
|
||||
"community_presets_show_again": "Show community presets",
|
||||
"community_presets_empty": "No presets loaded. Rebuild the bundle or add public/community_interfaces.json.",
|
||||
"find_more_nodes": "Find more nodes",
|
||||
"quick_import": "Quick Import",
|
||||
"quick_import_paste_hint": "Paste raw config",
|
||||
"quick_import_placeholder": "[[Interface Name]]\ntype = TCPClientInterface\ntarget_host = 1.2.3.4",
|
||||
"quick_import_apply": "Apply: {name}"
|
||||
},
|
||||
"map": {
|
||||
"title": "Map",
|
||||
@@ -870,7 +889,9 @@
|
||||
"pages": "Pages",
|
||||
"view": "View",
|
||||
"showing_range": "Showing {start} to {end} of {total} archives",
|
||||
"page_of": "Page {page} of {total_pages}"
|
||||
"page_of": "Page {page} of {total_pages}",
|
||||
"export_mu": "Export .mu",
|
||||
"export_selected_mu": "Export .mu ({count})"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Documentation",
|
||||
|
||||
@@ -210,7 +210,13 @@
|
||||
"blackhole_integration_enabled": "Integrazione Blackhole",
|
||||
"blackhole_integration_description": "Metti automaticamente in blackhole le identità a livello di trasporto Reticulum quando esili gli utenti in MeshChatX.",
|
||||
"announce_limits": "Limiti annunci",
|
||||
"announce_limits_description": "Numero massimo opzionale per aspetto. Lasciare vuoto per nessun limite. Protegge da attacchi flood su reti pubbliche.",
|
||||
"announce_limits_description": "Limita quanti annunci restano nel database per aspetto (si eliminano i più vecchi) e quanti vengono caricati per richiesta API. Limita anche ricerca e interfacce scoperte. Svuota un campo e salva per i default.",
|
||||
"announce_max_stored_heading": "Massimo salvato per aspetto (database)",
|
||||
"announce_fetch_limit_heading": "Dimensione pagina predefinita (API)",
|
||||
"announce_search_max_fetch": "Massimo annunci per ricerca",
|
||||
"announce_search_max_fetch_hint": "Righe massime lette dal database con ricerca testuale (per richiesta).",
|
||||
"discovered_interfaces_max_return": "Limite interfacce scoperte",
|
||||
"discovered_interfaces_max_return_hint": "Massimo elementi per interfacce scoperte e attive nell'interfaccia.",
|
||||
"announce_limit_lxmf": "LXMF",
|
||||
"announce_limit_nomadnet": "NomadNet",
|
||||
"announce_limit_prop": "Nodi prop",
|
||||
@@ -505,7 +511,20 @@
|
||||
"select_config_file": "Seleziona un file di configurazione",
|
||||
"select_at_least_one": "Seleziona almeno un'interfaccia da importare",
|
||||
"import_success": "Interfacce importate con successo. MeshChat deve essere riavviato per applicare le modifiche.",
|
||||
"failed_import_all": "Impossibile importare le interfacce"
|
||||
"failed_import_all": "Impossibile importare le interfacce",
|
||||
"add_interface_sidebar_a11y": "Collegamenti e preset community",
|
||||
"community_quick_start": "Avvio rapido community",
|
||||
"community_quick_start_hint": "Preset da directory.rns.recipes (elenchi online).",
|
||||
"community_quick_start_hide": "Nascondi suggerimenti",
|
||||
"community_use_preset": "Usa",
|
||||
"community_presets_hidden_hint": "I preset community sono nascosti. Mostra di nuovo qui o in Impostazioni.",
|
||||
"community_presets_show_again": "Mostra preset community",
|
||||
"community_presets_empty": "Nessun preset. Ricostruisci il bundle o aggiungi public/community_interfaces.json.",
|
||||
"find_more_nodes": "Trova altri nodi",
|
||||
"quick_import": "Import rapido",
|
||||
"quick_import_paste_hint": "Incolla config grezza",
|
||||
"quick_import_placeholder": "[[Interface Name]]\ntype = TCPClientInterface\ntarget_host = 1.2.3.4",
|
||||
"quick_import_apply": "Applica: {name}"
|
||||
},
|
||||
"map": {
|
||||
"title": "Mappa",
|
||||
@@ -916,7 +935,9 @@
|
||||
"pages": "Pagine",
|
||||
"view": "Visualizza",
|
||||
"showing_range": "Visualizzazione da {start} a {end} di {total} archivi",
|
||||
"page_of": "Pagina {page} di {total_pages}"
|
||||
"page_of": "Pagina {page} di {total_pages}",
|
||||
"export_mu": "Esporta .mu",
|
||||
"export_selected_mu": "Esporta .mu ({count})"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Documentazione",
|
||||
|
||||
@@ -207,7 +207,13 @@
|
||||
"blackhole_integration_enabled": "Интеграция Blackhole",
|
||||
"blackhole_integration_description": "Автоматически блокировать (blackhole) личности на транспортном уровне Reticulum при изгнании пользователей в MeshChatX.",
|
||||
"announce_limits": "Лимиты объявлений",
|
||||
"announce_limits_description": "Необязательный максимум по аспекту. Оставьте пустым для отсутствия лимита. Защита от флуд-атак в публичных сетях.",
|
||||
"announce_limits_description": "Ограничение числа объявлений в базе по аспекту (удаляются самые старые) и сколько загружается за запрос API. Также лимит поиска и списка обнаруженных интерфейсов. Очистите поле и сохраните для значений по умолчанию.",
|
||||
"announce_max_stored_heading": "Макс. в базе на аспект",
|
||||
"announce_fetch_limit_heading": "Размер страницы по умолчанию (API)",
|
||||
"announce_search_max_fetch": "Макс. объявлений для поиска",
|
||||
"announce_search_max_fetch_hint": "Максимум строк из БД при текстовом поиске (на запрос).",
|
||||
"discovered_interfaces_max_return": "Лимит обнаруженных интерфейсов",
|
||||
"discovered_interfaces_max_return_hint": "Максимум записей об обнаруженных и активных интерфейсах в интерфейсе.",
|
||||
"announce_limit_lxmf": "LXMF",
|
||||
"announce_limit_nomadnet": "NomadNet",
|
||||
"announce_limit_prop": "Prop-узлы",
|
||||
@@ -505,7 +511,20 @@
|
||||
"select_config_file": "Please select a configuration file",
|
||||
"select_at_least_one": "Please select at least one interface to import",
|
||||
"import_success": "Interfaces imported successfully. MeshChat must be restarted for these changes to take effect.",
|
||||
"failed_import_all": "Failed to import interfaces"
|
||||
"failed_import_all": "Failed to import interfaces",
|
||||
"add_interface_sidebar_a11y": "Ярлыки и пресеты сообщества",
|
||||
"community_quick_start": "Быстрый старт (сообщество)",
|
||||
"community_quick_start_hint": "Пресеты из онлайн-списков directory.rns.recipes.",
|
||||
"community_quick_start_hide": "Скрыть подсказки",
|
||||
"community_use_preset": "Применить",
|
||||
"community_presets_hidden_hint": "Пресеты сообщества скрыты. Показать снова здесь или в настройках.",
|
||||
"community_presets_show_again": "Показать пресеты сообщества",
|
||||
"community_presets_empty": "Пресеты не загружены. Пересоберите bundle или добавьте public/community_interfaces.json.",
|
||||
"find_more_nodes": "Найти узлы",
|
||||
"quick_import": "Быстрый импорт",
|
||||
"quick_import_paste_hint": "Вставьте сырую конфигурацию",
|
||||
"quick_import_placeholder": "[[Interface Name]]\ntype = TCPClientInterface\ntarget_host = 1.2.3.4",
|
||||
"quick_import_apply": "Применить: {name}"
|
||||
},
|
||||
"map": {
|
||||
"title": "Карта",
|
||||
@@ -827,7 +846,9 @@
|
||||
"pages": "Страницы",
|
||||
"view": "Просмотр",
|
||||
"showing_range": "Показано с {start} по {end} из {total} архивов",
|
||||
"page_of": "Страница {page} из {total_pages}"
|
||||
"page_of": "Страница {page} из {total_pages}",
|
||||
"export_mu": "Экспорт .mu",
|
||||
"export_selected_mu": "Экспорт .mu ({count})"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация",
|
||||
|
||||
Reference in New Issue
Block a user