feat(ui): enhance user experience with new features including QR code display, improved toast messages, and localized strings for various components
Some checks failed
CI / test-backend (push) Successful in 3s
CI / build-frontend (push) Successful in 1m48s
CI / test-backend (pull_request) Successful in 18s
CI / test-lang (push) Successful in 2m5s
Build and Publish Docker Image / build (pull_request) Has been skipped
CI / test-lang (pull_request) Successful in 1m14s
OSV-Scanner PR Scan / scan-pr (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 9m43s
CI / lint (push) Successful in 9m53s
CI / lint (pull_request) Successful in 9m49s
Build Test / Build and Test (pull_request) Successful in 12m57s
Tests / test (push) Successful in 14m2s
Benchmarks / benchmark (push) Successful in 14m29s
Build and Publish Docker Image / build-dev (pull_request) Successful in 19m25s
Tests / test (pull_request) Failing after 23m6s
Benchmarks / benchmark (pull_request) Successful in 29m13s
Build Test / Build and Test (push) Successful in 45m58s
Some checks failed
CI / test-backend (push) Successful in 3s
CI / build-frontend (push) Successful in 1m48s
CI / test-backend (pull_request) Successful in 18s
CI / test-lang (push) Successful in 2m5s
Build and Publish Docker Image / build (pull_request) Has been skipped
CI / test-lang (pull_request) Successful in 1m14s
OSV-Scanner PR Scan / scan-pr (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 9m43s
CI / lint (push) Successful in 9m53s
CI / lint (pull_request) Successful in 9m49s
Build Test / Build and Test (pull_request) Successful in 12m57s
Tests / test (push) Successful in 14m2s
Benchmarks / benchmark (push) Successful in 14m29s
Build and Publish Docker Image / build-dev (pull_request) Successful in 19m25s
Tests / test (pull_request) Failing after 23m6s
Benchmarks / benchmark (pull_request) Successful in 29m13s
Build Test / Build and Test (push) Successful in 45m58s
This commit is contained in:
@@ -133,7 +133,7 @@
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="flex h-full w-full flex-col overflow-y-auto border-r border-gray-200/70 bg-white dark:border-zinc-800 dark:bg-zinc-900 backdrop-blur"
|
||||
class="flex h-full w-full flex-col overflow-y-auto border-r border-gray-200/70 bg-white dark:border-zinc-800 dark:bg-zinc-900 backdrop-blur pt-16 sm:pt-0"
|
||||
>
|
||||
<!-- toggle button for desktop -->
|
||||
<div class="hidden sm:flex justify-end p-2 border-b border-gray-100 dark:border-zinc-800">
|
||||
@@ -353,8 +353,9 @@
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
|
||||
<div>{{ $t("app.identity_hash") }}</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
|
||||
:title="config.identity_hash"
|
||||
@click="copyValue(config.identity_hash, $t('app.identity_hash'))"
|
||||
>
|
||||
{{ config.identity_hash }}
|
||||
</div>
|
||||
@@ -362,11 +363,22 @@
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
|
||||
<div>{{ $t("app.lxmf_address") }}</div>
|
||||
<div
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
|
||||
:title="config.lxmf_address_hash"
|
||||
@click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
|
||||
>
|
||||
{{ config.lxmf_address_hash }}
|
||||
</div>
|
||||
<div class="flex items-center justify-end pt-1">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 rounded-lg text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
:title="$t('app.show_qr')"
|
||||
@click.stop="openLxmfQr"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="qrcode" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -461,6 +473,51 @@
|
||||
<ChangelogModal ref="changelogModal" :app-version="appInfo?.version" />
|
||||
<TutorialModal ref="tutorialModal" />
|
||||
|
||||
<!-- LXMF QR modal -->
|
||||
<div
|
||||
v-if="showLxmfQr"
|
||||
class="fixed inset-0 z-[190] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showLxmfQr = false"
|
||||
>
|
||||
<div class="w-full max-w-sm bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">LXMF Address QR</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-300 transition-colors"
|
||||
@click="showLxmfQr = false"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="close" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
v-if="lxmfQrDataUrl"
|
||||
:src="lxmfQrDataUrl"
|
||||
alt="LXMF QR"
|
||||
class="w-48 h-48 bg-white rounded-xl border border-gray-200 dark:border-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="config?.lxmf_address_hash"
|
||||
class="text-xs font-mono text-gray-700 dark:text-zinc-200 text-center break-words"
|
||||
>
|
||||
{{ config.lxmf_address_hash }}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1.5 text-xs font-semibold text-blue-600 dark:text-blue-400 hover:underline"
|
||||
@click="copyValue(config?.lxmf_address_hash, $t('app.lxmf_address'))"
|
||||
>
|
||||
{{ $t("common.copy") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- identity switching overlay -->
|
||||
<transition name="fade-blur">
|
||||
<div
|
||||
@@ -500,6 +557,7 @@ import Toast from "./Toast.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import ToastUtils from "../js/ToastUtils";
|
||||
import MaterialDesignIcon from "./MaterialDesignIcon.vue";
|
||||
import QRCode from "qrcode";
|
||||
import NotificationBell from "./NotificationBell.vue";
|
||||
import LanguageSelector from "./LanguageSelector.vue";
|
||||
import CallOverlay from "./call/CallOverlay.vue";
|
||||
@@ -554,6 +612,9 @@ export default {
|
||||
appInfo: null,
|
||||
hasCheckedForModals: false,
|
||||
|
||||
showLxmfQr: false,
|
||||
lxmfQrDataUrl: null,
|
||||
|
||||
activeCall: null,
|
||||
propagationNodeStatus: null,
|
||||
isCallEnded: false,
|
||||
@@ -893,13 +954,32 @@ export default {
|
||||
try {
|
||||
await window.axios.get(`/api/v1/announce`);
|
||||
} catch (e) {
|
||||
ToastUtils.error("failed to announce");
|
||||
ToastUtils.error(this.$t("app.failed_announce"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// fetch config so it updates last announced timestamp
|
||||
await this.getConfig();
|
||||
},
|
||||
async copyValue(value, label) {
|
||||
if (!value) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
ToastUtils.success(`${label} copied`);
|
||||
} catch {
|
||||
ToastUtils.success(value);
|
||||
}
|
||||
},
|
||||
async openLxmfQr() {
|
||||
if (!this.config?.lxmf_address_hash) return;
|
||||
try {
|
||||
const uri = `lxmf://${this.config.lxmf_address_hash}`;
|
||||
this.lxmfQrDataUrl = await QRCode.toDataURL(uri, { margin: 1, scale: 6 });
|
||||
this.showLxmfQr = true;
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.error"));
|
||||
}
|
||||
},
|
||||
async updateConfig(config, label = null) {
|
||||
try {
|
||||
WebSocketConnection.send(
|
||||
|
||||
@@ -287,6 +287,14 @@ export default {
|
||||
type: "action",
|
||||
action: "toggle-orbit",
|
||||
},
|
||||
{
|
||||
id: "action-bouncing-balls",
|
||||
title: "action_bouncing_balls",
|
||||
description: "action_bouncing_balls_desc",
|
||||
icon: "bounce",
|
||||
type: "action",
|
||||
action: "toggle-bouncing-balls",
|
||||
},
|
||||
{
|
||||
id: "action-getting-started",
|
||||
title: "action_getting_started",
|
||||
@@ -457,6 +465,8 @@ export default {
|
||||
});
|
||||
} else if (result.action === "toggle-orbit") {
|
||||
GlobalEmitter.emit("toggle-orbit");
|
||||
} else if (result.action === "toggle-bouncing-balls") {
|
||||
GlobalEmitter.emit("toggle-bouncing-balls");
|
||||
} else if (result.action === "show-tutorial") {
|
||||
GlobalEmitter.emit("show-tutorial");
|
||||
} else if (result.action === "show-changelog") {
|
||||
|
||||
@@ -3,23 +3,24 @@
|
||||
<v-card color="warning" class="pa-4">
|
||||
<v-card-title class="headline text-white">
|
||||
<v-icon start icon="mdi-alert-decagram" class="mr-2"></v-icon>
|
||||
Security Integrity Warning
|
||||
{{ $t("about.security_integrity") }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="text-white mt-2">
|
||||
<p v-if="integrity.backend && !integrity.backend.ok">
|
||||
<strong>Backend Tampering Detected!</strong><br />
|
||||
The application backend binary (unpacked from ASAR) appears to have been modified or replaced. This
|
||||
could indicate a malicious actor trying to compromise your mesh communication.
|
||||
<strong>{{ $t("about.tampering_detected") }}</strong
|
||||
><br />
|
||||
{{ $t("about.integrity_backend_error") }}
|
||||
</p>
|
||||
|
||||
<p v-if="integrity.data && !integrity.data.ok" class="mt-2">
|
||||
<strong>Data Tampering Detected!</strong><br />
|
||||
Your identities or database files appear to have been modified while the app was closed.
|
||||
<strong>{{ $t("about.tampering_detected") }}</strong
|
||||
><br />
|
||||
{{ $t("about.integrity_data_error") }}
|
||||
</p>
|
||||
|
||||
<v-expansion-panels v-if="issues.length > 0" variant="inset" class="mt-4">
|
||||
<v-expansion-panel title="Technical Details" bg-color="warning-darken-1">
|
||||
<v-expansion-panel :title="$t('about.technical_issues')" bg-color="warning-darken-1">
|
||||
<v-expansion-panel-text>
|
||||
<ul class="text-caption">
|
||||
<li v-for="(issue, index) in issues" :key="index">{{ issue }}</li>
|
||||
@@ -29,21 +30,20 @@
|
||||
</v-expansion-panels>
|
||||
|
||||
<p class="mt-4 text-caption">
|
||||
Proceed with caution. If you did not manually update or modify these files, your installation may be
|
||||
compromised.
|
||||
{{ $t("about.integrity_warning_footer") }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-checkbox
|
||||
v-model="dontShowAgain"
|
||||
label="I understand, do not show again for this version"
|
||||
:label="$t('app.do_not_show_again')"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="text-white"
|
||||
></v-checkbox>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" color="white" @click="close"> Continue Anyway </v-btn>
|
||||
<v-btn variant="text" color="white" @click="close"> {{ $t("common.continue") }} </v-btn>
|
||||
<v-btn
|
||||
v-if="integrity.data && !integrity.data.ok"
|
||||
variant="flat"
|
||||
@@ -51,7 +51,7 @@
|
||||
class="text-warning font-bold"
|
||||
@click="acknowledgeAndReset"
|
||||
>
|
||||
Acknowledge & Reset
|
||||
{{ $t("common.acknowledge_reset") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -104,10 +104,10 @@ export default {
|
||||
async acknowledgeAndReset() {
|
||||
try {
|
||||
await window.axios.post("/api/v1/app/integrity/acknowledge");
|
||||
ToastUtils.success("Integrity issues acknowledged and manifest reset");
|
||||
ToastUtils.success(this.$t("about.integrity_acknowledged_reset"));
|
||||
this.visible = false;
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to acknowledge integrity issues");
|
||||
ToastUtils.error(this.$t("about.failed_acknowledge_integrity"));
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<!-- content -->
|
||||
<div class="flex-1 mr-2 text-sm font-medium text-gray-900 dark:text-zinc-100">
|
||||
{{ toast.message }}
|
||||
{{ $t(toast.message) }}
|
||||
</div>
|
||||
|
||||
<!-- close button -->
|
||||
|
||||
@@ -1405,12 +1405,12 @@ export default {
|
||||
autoconnect_discovered_interfaces: 3, // default to 3 slots
|
||||
};
|
||||
await window.axios.patch(`/api/v1/reticulum/discovery`, payload);
|
||||
ToastUtils.success("Community discovery enabled");
|
||||
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
|
||||
this.discoveryOption = "yes";
|
||||
this.nextStep();
|
||||
} catch (e) {
|
||||
console.error("Failed to enable discovery:", e);
|
||||
ToastUtils.error("Failed to enable discovery");
|
||||
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
|
||||
} finally {
|
||||
this.savingDiscovery = false;
|
||||
}
|
||||
@@ -1527,7 +1527,7 @@ export default {
|
||||
ElectronUtils.relaunch();
|
||||
} else {
|
||||
if (this.interfaceAddedViaTutorial) {
|
||||
ToastUtils.info("Restart the application/container to apply changes.");
|
||||
ToastUtils.info(this.$t("tutorial.ready_desc"));
|
||||
}
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
@@ -46,9 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-10 pt-8 border-t border-gray-100 dark:border-zinc-800 flex flex-col md:flex-row md:items-center justify-between gap-6"
|
||||
>
|
||||
<div class="mt-10 pt-8 border-t border-gray-100 dark:border-zinc-800 flex flex-col gap-6">
|
||||
<div class="text-gray-600 dark:text-zinc-400 max-w-xl text-lg leading-relaxed">
|
||||
A secure, resilient, and beautiful communications platform powered by the
|
||||
<a
|
||||
@@ -58,6 +56,81 @@
|
||||
>Reticulum Network Stack</a
|
||||
>.
|
||||
</div>
|
||||
|
||||
<!-- Contact Developer Card -->
|
||||
<div class="glass-card !p-5 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div
|
||||
class="text-xs font-black uppercase tracking-widest text-gray-500 dark:text-zinc-500"
|
||||
>
|
||||
Contact Developer
|
||||
</div>
|
||||
<v-icon
|
||||
:icon="showContactDev ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
size="18"
|
||||
></v-icon>
|
||||
</div>
|
||||
<button
|
||||
class="w-full text-left flex items-center justify-between px-4 py-2 rounded-xl bg-blue-500/10 hover:bg-blue-500/20 text-blue-600 dark:text-blue-400 border border-blue-500/20 transition-all"
|
||||
@click="showContactDev = !showContactDev"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<v-icon icon="mdi-account-card-details" size="18"></v-icon>
|
||||
<span class="text-xs font-black uppercase tracking-widest">Details</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showContactDev"
|
||||
class="mt-4 p-5 rounded-2xl bg-white/50 dark:bg-zinc-950/50 border border-gray-100 dark:border-zinc-800 space-y-4"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<div class="text-[9px] font-black text-gray-400 uppercase tracking-widest">
|
||||
LXMF Address
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="text-[11px] font-mono bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded-lg break-all"
|
||||
>7cc8d66b4f6a0e0e49d34af7f6077b5a</code
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:text-blue-500 transition"
|
||||
@click="copyValue('7cc8d66b4f6a0e0e49d34af7f6077b5a', 'LXMF Address')"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="14"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="text-[9px] font-black text-gray-400 uppercase tracking-widest">
|
||||
Alternate
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="text-[11px] font-mono bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded-lg break-all"
|
||||
>43d3309adf27fc446556121b553b56a6</code
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:text-blue-500 transition"
|
||||
@click="
|
||||
copyValue('43d3309adf27fc446556121b553b56a6', 'Alternate Address')
|
||||
"
|
||||
>
|
||||
<v-icon icon="mdi-content-copy" size="14"></v-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-xs font-bold text-gray-500 dark:text-white italic bg-blue-500/5 p-3 rounded-xl border border-blue-500/10 flex items-center gap-2"
|
||||
>
|
||||
<v-icon icon="mdi-information-outline" size="14" class="text-blue-500"></v-icon>
|
||||
Send to propagation node if you cant reach me!
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-6 shrink-0">
|
||||
<div class="text-right">
|
||||
<div
|
||||
@@ -869,6 +942,7 @@ export default {
|
||||
chromeVersion: null,
|
||||
nodeVersion: null,
|
||||
showIdentityPaste: false,
|
||||
showContactDev: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -928,23 +1002,23 @@ export default {
|
||||
}
|
||||
},
|
||||
async deleteSnapshot(filename) {
|
||||
if (!(await DialogUtils.confirm("Are you sure you want to delete this snapshot?"))) return;
|
||||
if (!(await DialogUtils.confirm(this.$t("about.delete_snapshot_confirm")))) return;
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/database/snapshots/${filename}`);
|
||||
ToastUtils.success("Snapshot deleted");
|
||||
ToastUtils.success(this.$t("about.snapshot_deleted"));
|
||||
await this.listSnapshots();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete snapshot");
|
||||
ToastUtils.error(this.$t("about.failed_delete_snapshot"));
|
||||
}
|
||||
},
|
||||
async deleteBackup(filename) {
|
||||
if (!(await DialogUtils.confirm("Are you sure you want to delete this backup?"))) return;
|
||||
if (!(await DialogUtils.confirm(this.$t("about.delete_backup_confirm")))) return;
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/database/backups/${filename}`);
|
||||
ToastUtils.success("Backup deleted");
|
||||
ToastUtils.success(this.$t("about.backup_deleted"));
|
||||
await this.listAutoBackups();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete backup");
|
||||
ToastUtils.error(this.$t("about.failed_delete_backup"));
|
||||
}
|
||||
},
|
||||
async nextSnapshots() {
|
||||
@@ -990,23 +1064,19 @@ export default {
|
||||
}
|
||||
},
|
||||
async restoreFromSnapshot(path) {
|
||||
if (
|
||||
!(await DialogUtils.confirm(
|
||||
"Are you sure you want to restore this snapshot? This will overwrite the current database and require an app relaunch."
|
||||
))
|
||||
) {
|
||||
if (!(await DialogUtils.confirm(this.$t("about.restore_snapshot_confirm")))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await window.axios.post("/api/v1/database/restore", { path });
|
||||
if (response.data.status === "success") {
|
||||
ToastUtils.success("Database restored. Relaunching...");
|
||||
ToastUtils.success(this.$t("about.database_restored"));
|
||||
if (this.isElectron) {
|
||||
setTimeout(() => ElectronUtils.relaunch(), 2000);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error("Failed to restore snapshot");
|
||||
ToastUtils.error(this.$t("about.failed_restore_snapshot"));
|
||||
}
|
||||
},
|
||||
async getAppInfo() {
|
||||
@@ -1026,17 +1096,13 @@ export default {
|
||||
}
|
||||
},
|
||||
async acknowledgeIntegrity() {
|
||||
if (
|
||||
await DialogUtils.confirm(
|
||||
"Are you sure you want to acknowledge these integrity issues? This will update the security manifest to match the current state of your files."
|
||||
)
|
||||
) {
|
||||
if (await DialogUtils.confirm(this.$t("about.integrity_acknowledge_confirm"))) {
|
||||
try {
|
||||
await window.axios.post("/api/v1/app/integrity/acknowledge");
|
||||
ToastUtils.success("Integrity issues acknowledged");
|
||||
ToastUtils.success(this.$t("about.integrity_acknowledged"));
|
||||
await this.getAppInfo();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to acknowledge integrity issues");
|
||||
ToastUtils.error(this.$t("about.failed_acknowledge_integrity"));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1196,7 +1262,7 @@ export default {
|
||||
if (this.isElectron) {
|
||||
ElectronUtils.shutdown();
|
||||
} else {
|
||||
ToastUtils.success("Shutdown command sent to server.");
|
||||
ToastUtils.success(this.$t("about.shutdown_sent"));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1253,7 +1319,7 @@ export default {
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
this.identityBackupMessage = "Identity downloaded. Keep it secret.";
|
||||
ToastUtils.success("Identity key file exported");
|
||||
ToastUtils.success(this.$t("about.identity_exported"));
|
||||
} catch {
|
||||
this.identityBackupError = "Failed to download identity";
|
||||
}
|
||||
@@ -1270,7 +1336,7 @@ export default {
|
||||
}
|
||||
await navigator.clipboard.writeText(this.identityBase32);
|
||||
this.identityBase32Message = "Identity copied. Clear your clipboard after use.";
|
||||
ToastUtils.success("Identity Base32 key copied to clipboard");
|
||||
ToastUtils.success(this.$t("about.identity_copied"));
|
||||
} catch {
|
||||
this.identityBase32Error = "Failed to copy identity";
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<MaterialDesignIcon icon-name="block-helper" class="size-6 text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">Banished</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Manage Banished users and nodes</p>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{{ $t("banishment.title") }}</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t("banishment.description") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
||||
placeholder="Search by hash or display name..."
|
||||
:placeholder="$t('banishment.search_placeholder')"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="p-2 text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors"
|
||||
title="Refresh"
|
||||
:title="$t('common.refresh')"
|
||||
@click="loadBlockedDestinations"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<div v-if="isLoading && blockedItems.length === 0" class="flex flex-col items-center justify-center h-64">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Loading banished items...</p>
|
||||
<p class="text-gray-500 dark:text-gray-400">{{ $t("banishment.loading_items") }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -53,13 +53,9 @@
|
||||
<div class="p-4 bg-gray-100 dark:bg-zinc-800 rounded-full mb-4 text-gray-400 dark:text-zinc-600">
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-12" />
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">No banished items</h3>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ $t("banishment.no_items") }}</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400 max-w-sm mx-auto">
|
||||
{{
|
||||
searchQuery
|
||||
? "No banished items match your search."
|
||||
: "You haven't banished any users or nodes yet."
|
||||
}}
|
||||
{{ searchQuery ? $t("nomadnet.no_search_results_peers") : $t("nomadnet.no_announces_yet") }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -85,19 +81,19 @@
|
||||
class="text-base font-semibold text-gray-900 dark:text-white break-words"
|
||||
:title="item.display_name"
|
||||
>
|
||||
{{ item.display_name || "Unknown" }}
|
||||
{{ item.display_name || $t("call.unknown") }}
|
||||
</h4>
|
||||
<span
|
||||
v-if="item.is_node"
|
||||
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded"
|
||||
>
|
||||
Node
|
||||
{{ $t("banishment.node") }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded"
|
||||
>
|
||||
User
|
||||
{{ $t("banishment.user") }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.is_rns_blackholed"
|
||||
@@ -116,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.created_at" class="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
Banished {{ formatTimeAgo(item.created_at) }}
|
||||
{{ $t("banishment.banished_at") }} {{ formatTimeAgo(item.created_at) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.rns_source"
|
||||
@@ -137,7 +133,7 @@
|
||||
@click="onUnblock(item)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="check-circle" class="size-5" />
|
||||
<span>Lift Banishment</span>
|
||||
<span>{{ $t("banishment.lift_banishment") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +208,7 @@ export default {
|
||||
}
|
||||
|
||||
const processItem = async (hash, data = {}) => {
|
||||
let displayName = "Unknown";
|
||||
let displayName = this.$t("call.unknown");
|
||||
let isNode = false;
|
||||
|
||||
try {
|
||||
@@ -226,7 +222,7 @@ export default {
|
||||
|
||||
if (announceResponse.data.announces && announceResponse.data.announces.length > 0) {
|
||||
const announce = announceResponse.data.announces[0];
|
||||
displayName = announce.display_name || "Unknown";
|
||||
displayName = announce.display_name || this.$t("call.unknown");
|
||||
isNode = announce.aspect === "nomadnetwork.node";
|
||||
}
|
||||
} catch {
|
||||
@@ -261,7 +257,7 @@ export default {
|
||||
this.reticulumBlackholedItems = rnsItems;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastUtils.error("Failed to load banished destinations");
|
||||
ToastUtils.error(this.$t("banishment.failed_load_banished"));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@@ -269,7 +265,7 @@ export default {
|
||||
async onUnblock(item) {
|
||||
if (
|
||||
!(await DialogUtils.confirm(
|
||||
`Are you sure you want to lift the banishment for ${item.display_name || item.destination_hash}?`
|
||||
this.$t("banishment.lift_banishment_confirm", { name: item.display_name || item.destination_hash })
|
||||
))
|
||||
) {
|
||||
return;
|
||||
@@ -278,10 +274,10 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/blocked-destinations/${item.destination_hash}`);
|
||||
await this.loadBlockedDestinations();
|
||||
ToastUtils.success("Banishment lifted successfully");
|
||||
ToastUtils.success(this.$t("banishment.banishment_lifted"));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastUtils.error("Failed to lift banishment");
|
||||
ToastUtils.error(this.$t("banishment.failed_lift_banishment"));
|
||||
}
|
||||
},
|
||||
onSearchInput() {},
|
||||
|
||||
@@ -409,7 +409,7 @@ export default {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/answer");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to answer call");
|
||||
ToastUtils.error(this.$t("call.failed_to_answer_call"));
|
||||
}
|
||||
},
|
||||
async hangupCall() {
|
||||
@@ -417,15 +417,15 @@ export default {
|
||||
this.$emit("hangup");
|
||||
await window.axios.get("/api/v1/telephone/hangup");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to hangup call");
|
||||
ToastUtils.error(this.$t("call.failed_to_hangup_call"));
|
||||
}
|
||||
},
|
||||
async sendToVoicemail() {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/send-to-voicemail");
|
||||
ToastUtils.success("Call sent to voicemail");
|
||||
ToastUtils.success(this.$t("call.call_sent_to_voicemail"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to send call to voicemail");
|
||||
ToastUtils.error(this.$t("call.failed_to_send_to_voicemail"));
|
||||
}
|
||||
},
|
||||
async toggleMicrophone() {
|
||||
@@ -449,7 +449,7 @@ export default {
|
||||
this.isMicMuting = false;
|
||||
// Revert on error
|
||||
this.localMicMuted = !this.localMicMuted;
|
||||
ToastUtils.error("Failed to toggle microphone");
|
||||
ToastUtils.error(this.$t("call.failed_to_toggle_microphone"));
|
||||
}
|
||||
},
|
||||
async toggleSpeaker() {
|
||||
@@ -473,7 +473,7 @@ export default {
|
||||
this.isSpeakerMuting = false;
|
||||
// Revert on error
|
||||
this.localSpeakerMuted = !this.localSpeakerMuted;
|
||||
ToastUtils.error("Failed to toggle speaker");
|
||||
ToastUtils.error(this.$t("call.failed_to_toggle_speaker"));
|
||||
}
|
||||
},
|
||||
async playLatestVoicemail() {
|
||||
|
||||
@@ -2481,7 +2481,7 @@ export default {
|
||||
this.refreshAudioDevices();
|
||||
} catch (err) {
|
||||
console.error("Web audio failed", err);
|
||||
ToastUtils.error("Web audio not available");
|
||||
ToastUtils.error(this.$t("call.web_audio_not_available"));
|
||||
this.stopWebAudio();
|
||||
}
|
||||
},
|
||||
@@ -2596,9 +2596,9 @@ export default {
|
||||
if (response.data?.config) {
|
||||
this.config = response.data.config;
|
||||
}
|
||||
ToastUtils.success("Settings saved");
|
||||
ToastUtils.success(this.$t("call.settings_saved"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save settings");
|
||||
ToastUtils.error(this.$t("call.failed_to_save_settings"));
|
||||
}
|
||||
},
|
||||
async getAudioProfiles() {
|
||||
@@ -2641,6 +2641,8 @@ export default {
|
||||
await this.ensureWebAudio(response.data.web_audio);
|
||||
}
|
||||
|
||||
this.hydrateContactVisuals();
|
||||
|
||||
// If call just ended, refresh history and show ended state
|
||||
if (oldCall != null && this.activeCall == null) {
|
||||
this.getHistory();
|
||||
@@ -2706,6 +2708,7 @@ export default {
|
||||
}
|
||||
|
||||
this.hasMoreCallHistory = newItems.length === this.callHistoryLimit;
|
||||
this.hydrateContactVisuals();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -2769,7 +2772,7 @@ export default {
|
||||
}
|
||||
ToastUtils.success(value ? "Do Not Disturb enabled" : "Do Not Disturb disabled");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to update Do Not Disturb status");
|
||||
ToastUtils.error(this.$t("call.failed_to_update_dnd"));
|
||||
}
|
||||
},
|
||||
async toggleAllowCallsFromContactsOnly(value) {
|
||||
@@ -2782,7 +2785,7 @@ export default {
|
||||
}
|
||||
ToastUtils.success(value ? "Calls limited to contacts" : "Calls allowed from everyone");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to update call settings");
|
||||
ToastUtils.error(this.$t("call.failed_to_update_call_settings"));
|
||||
}
|
||||
},
|
||||
async toggleCallRecording(value) {
|
||||
@@ -2795,7 +2798,7 @@ export default {
|
||||
}
|
||||
ToastUtils.success(value ? "Call recording enabled" : "Call recording disabled");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to update call recording status");
|
||||
ToastUtils.error(this.$t("call.failed_to_update_recording_status"));
|
||||
}
|
||||
},
|
||||
async clearHistory() {
|
||||
@@ -2803,10 +2806,10 @@ export default {
|
||||
try {
|
||||
await window.axios.delete("/api/v1/telephone/history");
|
||||
this.callHistory = [];
|
||||
ToastUtils.success("Call history cleared");
|
||||
ToastUtils.success(this.$t("call.call_history_cleared"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to clear call history");
|
||||
ToastUtils.error(this.$t("call.failed_to_clear_call_history"));
|
||||
}
|
||||
},
|
||||
async blockIdentity(hash) {
|
||||
@@ -2815,10 +2818,10 @@ export default {
|
||||
await window.axios.post("/api/v1/blocked-destinations", {
|
||||
destination_hash: hash,
|
||||
});
|
||||
ToastUtils.success("Identity banished");
|
||||
ToastUtils.success(this.$t("call.identity_banished"));
|
||||
this.getHistory();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to banish identity");
|
||||
ToastUtils.error(this.$t("call.failed_to_banish_identity"));
|
||||
}
|
||||
},
|
||||
async getVoicemailStatus() {
|
||||
@@ -2976,6 +2979,7 @@ export default {
|
||||
params: { search: this.contactsSearch },
|
||||
});
|
||||
this.contacts = response.data.contacts || (Array.isArray(response.data) ? response.data : []);
|
||||
this.hydrateContactVisuals();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -2986,6 +2990,40 @@ export default {
|
||||
this.getContacts();
|
||||
}, 300);
|
||||
},
|
||||
hydrateContactVisuals() {
|
||||
const map = {};
|
||||
this.contacts.forEach((c) => {
|
||||
if (!c) return;
|
||||
const image = c.custom_image;
|
||||
const keys = [c.remote_identity_hash, c.lxmf_address, c.lxst_address].filter(Boolean);
|
||||
keys.forEach((k) => {
|
||||
map[k] = image;
|
||||
});
|
||||
});
|
||||
|
||||
const applyImage = (target) => {
|
||||
if (!target) return;
|
||||
const key =
|
||||
target.remote_identity_hash || target.remote_destination_hash || target.remote_telephony_hash;
|
||||
if (key && map[key]) {
|
||||
target.custom_image = map[key];
|
||||
}
|
||||
};
|
||||
|
||||
applyImage(this.activeCall);
|
||||
applyImage(this.lastCall);
|
||||
|
||||
if (Array.isArray(this.callHistory) && this.callHistory.length > 0) {
|
||||
this.callHistory = this.callHistory.map((entry) => {
|
||||
const key =
|
||||
entry.remote_identity_hash || entry.remote_destination_hash || entry.remote_telephony_hash;
|
||||
if (key && map[key]) {
|
||||
return { ...entry, contact_image: map[key] };
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
},
|
||||
openAddContactModal() {
|
||||
this.editingContact = null;
|
||||
this.contactForm = {
|
||||
@@ -3013,7 +3051,7 @@ export default {
|
||||
},
|
||||
async saveContact(contact) {
|
||||
if (!contact.name || !contact.remote_identity_hash) {
|
||||
ToastUtils.error("Name and identity hash required");
|
||||
ToastUtils.error(this.$t("call.name_and_hash_required"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -3023,10 +3061,10 @@ export default {
|
||||
contact.clear_image = true;
|
||||
}
|
||||
await window.axios.patch(`/api/v1/telephone/contacts/${contact.id}`, contact);
|
||||
ToastUtils.success("Contact updated");
|
||||
ToastUtils.success(this.$t("call.contact_updated"));
|
||||
} else {
|
||||
await window.axios.post("/api/v1/telephone/contacts", contact);
|
||||
ToastUtils.success("Contact added");
|
||||
ToastUtils.success(this.$t("call.contact_added"));
|
||||
}
|
||||
this.isContactModalOpen = false;
|
||||
this.getContacts();
|
||||
@@ -3038,10 +3076,10 @@ export default {
|
||||
if (!confirm("Are you sure you want to delete this contact?")) return;
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/telephone/contacts/${contactId}`);
|
||||
ToastUtils.success("Contact deleted");
|
||||
ToastUtils.success(this.$t("call.contact_deleted"));
|
||||
this.getContacts();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete contact");
|
||||
ToastUtils.error(this.$t("call.failed_to_delete_contact"));
|
||||
}
|
||||
},
|
||||
onContactImageChange(event) {
|
||||
@@ -3069,17 +3107,17 @@ export default {
|
||||
async copyHash(hash) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
ToastUtils.success("Hash copied to clipboard");
|
||||
ToastUtils.success(this.$t("call.hash_copied"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to copy hash");
|
||||
ToastUtils.error(this.$t("call.failed_to_copy_hash"));
|
||||
}
|
||||
},
|
||||
async generateGreeting() {
|
||||
this.isGeneratingGreeting = true;
|
||||
try {
|
||||
await window.axios.post("/api/v1/telephone/voicemail/generate-greeting");
|
||||
ToastUtils.success("Greeting generated successfully");
|
||||
ToastUtils.success(this.$t("call.greeting_generated_successfully"));
|
||||
await this.getVoicemailStatus();
|
||||
} catch (e) {
|
||||
ToastUtils.error(e.response?.data?.message || "Failed to generate greeting");
|
||||
@@ -3101,7 +3139,7 @@ export default {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
ToastUtils.success("Greeting uploaded successfully");
|
||||
ToastUtils.success(this.$t("call.greeting_uploaded_successfully"));
|
||||
await this.getVoicemailStatus();
|
||||
} catch (e) {
|
||||
ToastUtils.error(e.response?.data?.message || "Failed to upload greeting");
|
||||
@@ -3115,10 +3153,10 @@ export default {
|
||||
|
||||
try {
|
||||
await window.axios.delete("/api/v1/telephone/voicemail/greeting");
|
||||
ToastUtils.success("Greeting deleted");
|
||||
ToastUtils.success(this.$t("call.greeting_deleted"));
|
||||
await this.getVoicemailStatus();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete greeting");
|
||||
ToastUtils.error(this.$t("call.failed_to_delete_greeting"));
|
||||
}
|
||||
},
|
||||
async startRecordingGreetingMic() {
|
||||
@@ -3126,16 +3164,16 @@ export default {
|
||||
await window.axios.post("/api/v1/telephone/voicemail/greeting/record/start");
|
||||
await this.getVoicemailStatus();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to start recording greeting");
|
||||
ToastUtils.error(this.$t("call.failed_to_start_recording_greeting"));
|
||||
}
|
||||
},
|
||||
async stopRecordingGreetingMic() {
|
||||
try {
|
||||
await window.axios.post("/api/v1/telephone/voicemail/greeting/record/stop");
|
||||
await this.getVoicemailStatus();
|
||||
ToastUtils.success("Greeting recorded from mic");
|
||||
ToastUtils.success(this.$t("call.greeting_recorded_from_mic"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to stop recording greeting");
|
||||
ToastUtils.error(this.$t("call.failed_to_stop_recording_greeting"));
|
||||
}
|
||||
},
|
||||
async playVoicemail(voicemail) {
|
||||
@@ -3195,9 +3233,9 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/telephone/voicemails/${voicemailId}`);
|
||||
this.getVoicemails();
|
||||
ToastUtils.success("Voicemail deleted");
|
||||
ToastUtils.success(this.$t("call.voicemail_deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete voicemail");
|
||||
ToastUtils.error(this.$t("call.failed_to_delete_voicemail"));
|
||||
}
|
||||
},
|
||||
async getRecordings() {
|
||||
@@ -3244,7 +3282,7 @@ export default {
|
||||
await this.audioPlayer.play();
|
||||
} catch (e) {
|
||||
console.error("Failed to play recording:", e);
|
||||
ToastUtils.error("Failed to load recording audio");
|
||||
ToastUtils.error(this.$t("call.failed_to_load_recording"));
|
||||
this.playingRecordingId = null;
|
||||
this.playingSide = null;
|
||||
}
|
||||
@@ -3254,9 +3292,9 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/telephone/recordings/${recordingId}`);
|
||||
this.getRecordings();
|
||||
ToastUtils.success("Recording deleted");
|
||||
ToastUtils.success(this.$t("call.recording_deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete recording");
|
||||
ToastUtils.error(this.$t("call.failed_to_delete_recording"));
|
||||
}
|
||||
},
|
||||
async playGreeting() {
|
||||
@@ -3282,7 +3320,7 @@ export default {
|
||||
},
|
||||
async call(identityHash) {
|
||||
if (!identityHash) {
|
||||
ToastUtils.error("Enter an identity to call");
|
||||
ToastUtils.error(this.$t("call.enter_identity_hash_to_call_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3360,7 +3398,7 @@ export default {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/answer");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to answer call");
|
||||
ToastUtils.error(this.$t("call.failed_to_answer_call"));
|
||||
}
|
||||
},
|
||||
async hangupCall() {
|
||||
@@ -3370,22 +3408,22 @@ export default {
|
||||
}
|
||||
await window.axios.get("/api/v1/telephone/hangup");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to hangup call");
|
||||
ToastUtils.error(this.$t("call.failed_to_hangup_call"));
|
||||
}
|
||||
},
|
||||
async sendToVoicemail() {
|
||||
try {
|
||||
await window.axios.get("/api/v1/telephone/send-to-voicemail");
|
||||
ToastUtils.success("Call sent to voicemail");
|
||||
ToastUtils.success(this.$t("call.call_sent_to_voicemail"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to send call to voicemail");
|
||||
ToastUtils.error(this.$t("call.failed_to_send_to_voicemail"));
|
||||
}
|
||||
},
|
||||
async switchAudioProfile(audioProfileId) {
|
||||
try {
|
||||
await window.axios.get(`/api/v1/telephone/switch-audio-profile/${audioProfileId}`);
|
||||
} catch {
|
||||
ToastUtils.error("Failed to switch audio profile");
|
||||
ToastUtils.error(this.$t("call.failed_to_switch_audio_profile"));
|
||||
}
|
||||
},
|
||||
async toggleMicrophone() {
|
||||
@@ -3412,7 +3450,7 @@ export default {
|
||||
this.isMicMuting = false;
|
||||
// Revert on error
|
||||
this.localMicMuted = !this.localMicMuted;
|
||||
ToastUtils.error("Failed to toggle microphone");
|
||||
ToastUtils.error(this.$t("call.failed_to_toggle_microphone"));
|
||||
}
|
||||
},
|
||||
async toggleSpeaker() {
|
||||
@@ -3439,7 +3477,7 @@ export default {
|
||||
this.isSpeakerMuting = false;
|
||||
// Revert on error
|
||||
this.localSpeakerMuted = !this.localSpeakerMuted;
|
||||
ToastUtils.error("Failed to toggle speaker");
|
||||
ToastUtils.error(this.$t("call.failed_to_toggle_speaker"));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -217,7 +217,7 @@ export default {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to load audio:", e);
|
||||
ToastUtils.error("Failed to load audio for editing");
|
||||
ToastUtils.error(this.$t("call.failed_load_audio_edit"));
|
||||
this.$emit("close");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -418,12 +418,12 @@ export default {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
|
||||
ToastUtils.success("Ringtone saved successfully");
|
||||
ToastUtils.success(this.$t("call.ringtone_saved"));
|
||||
this.$emit("saved");
|
||||
this.$emit("close");
|
||||
} catch (e) {
|
||||
console.error("Failed to save ringtone:", e);
|
||||
ToastUtils.error("Failed to save edited ringtone");
|
||||
ToastUtils.error(this.$t("call.failed_save_ringtone"));
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ export default {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to load audio:", e);
|
||||
ToastUtils.error("Failed to load audio for editing");
|
||||
ToastUtils.error(this.$t("call.failed_load_audio_edit"));
|
||||
this.$emit("close");
|
||||
} finally {
|
||||
this.loading = false;
|
||||
@@ -449,12 +449,12 @@ export default {
|
||||
// await window.axios.delete(`/api/v1/telephone/ringtones/${this.ringtone.id}`);
|
||||
}
|
||||
|
||||
ToastUtils.success("Ringtone saved successfully");
|
||||
ToastUtils.success(this.$t("call.ringtone_saved"));
|
||||
this.$emit("saved");
|
||||
this.$emit("close");
|
||||
} catch (e) {
|
||||
console.error("Failed to save ringtone:", e);
|
||||
ToastUtils.error("Failed to save edited ringtone");
|
||||
ToastUtils.error(this.$t("call.failed_save_ringtone"));
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ export default {
|
||||
this.total = response.data.total;
|
||||
} catch (e) {
|
||||
console.log("Failed to fetch logs", e);
|
||||
if (!silent) ToastUtils.error("Failed to fetch logs");
|
||||
if (!silent) ToastUtils.error(this.$t("debug.failed_fetch_logs"));
|
||||
} finally {
|
||||
if (!silent) this.loading = false;
|
||||
}
|
||||
@@ -259,9 +259,9 @@ export default {
|
||||
.join("\n");
|
||||
try {
|
||||
await navigator.clipboard.writeText(logText);
|
||||
ToastUtils.success("Logs on this page copied to clipboard");
|
||||
ToastUtils.success(this.$t("debug.logs_copied"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to copy logs");
|
||||
ToastUtils.error(this.$t("debug.failed_copy_logs"));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -691,7 +691,7 @@ export default {
|
||||
await this.updateDocs();
|
||||
} catch (error) {
|
||||
console.error("Failed to add alternate source:", error);
|
||||
ToastUtils.error("Failed to update documentation sources");
|
||||
ToastUtils.error(this.$t("docs.failed_update_docs"));
|
||||
}
|
||||
},
|
||||
async switchVersion(version) {
|
||||
@@ -743,10 +743,10 @@ export default {
|
||||
navigator.clipboard
|
||||
.writeText(url)
|
||||
.then(() => {
|
||||
ToastUtils.success("Documentation link copied to clipboard");
|
||||
ToastUtils.success(this.$t("docs.docs_link_copied"));
|
||||
})
|
||||
.catch(() => {
|
||||
ToastUtils.error("Failed to copy link");
|
||||
ToastUtils.error(this.$t("docs.failed_copy_link"));
|
||||
});
|
||||
},
|
||||
async setLanguage(langCode) {
|
||||
|
||||
@@ -152,6 +152,7 @@
|
||||
<script>
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
|
||||
export default {
|
||||
name: "ForwarderPage",
|
||||
@@ -206,8 +207,8 @@ export default {
|
||||
this.newRule.forward_to_hash = "";
|
||||
this.newRule.source_filter_hash = "";
|
||||
},
|
||||
deleteRule(id) {
|
||||
if (confirm(this.$t("forwarder.delete_confirm"))) {
|
||||
async deleteRule(id) {
|
||||
if (await DialogUtils.confirm(this.$t("forwarder.delete_confirm"))) {
|
||||
WebSocketConnection.send(
|
||||
JSON.stringify({
|
||||
type: "lxmf.forwarding.rule.delete",
|
||||
|
||||
@@ -1505,7 +1505,7 @@ export default {
|
||||
const response = await window.axios.patch("/api/v1/config", config);
|
||||
this.config = response.data.config;
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to save config!");
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
@@ -1560,10 +1560,10 @@ export default {
|
||||
};
|
||||
|
||||
await window.axios.patch(`/api/v1/reticulum/discovery`, payload);
|
||||
ToastUtils.success("Discovery settings saved");
|
||||
ToastUtils.success(this.$t("interfaces.discovery_settings_saved"));
|
||||
await this.loadReticulumDiscoveryConfig();
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to save discovery settings");
|
||||
ToastUtils.error(this.$t("interfaces.failed_save_discovery"));
|
||||
console.log(e);
|
||||
} finally {
|
||||
this.savingDiscovery = false;
|
||||
@@ -1594,7 +1594,7 @@ export default {
|
||||
// find interface, else show error and redirect to interfaces
|
||||
const iface = interfaces[interfaceName];
|
||||
if (!iface) {
|
||||
DialogUtils.alert("The selected interface for editing could not be found.");
|
||||
DialogUtils.alert(this.$t("interfaces.interface_not_found"));
|
||||
this.$router.push({
|
||||
name: "interfaces",
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ export default {
|
||||
// ensure there are some interfaces available to import
|
||||
if (!response.data.interfaces || response.data.interfaces.length === 0) {
|
||||
this.clearSelectedFile();
|
||||
DialogUtils.alert("No interfaces were found in the selected configuration file");
|
||||
DialogUtils.alert(this.$t("interfaces.no_interfaces_found_config"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ export default {
|
||||
this.selectAllInterfaces();
|
||||
} catch (e) {
|
||||
this.clearSelectedFile();
|
||||
DialogUtils.alert("Failed to parse configuration file");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_parse_config"));
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
@@ -237,13 +237,13 @@ export default {
|
||||
async importSelectedInterfaces() {
|
||||
// ensure user selected a file to import from
|
||||
if (!this.selectedFile) {
|
||||
DialogUtils.alert("Please select a configuration file");
|
||||
DialogUtils.alert(this.$t("interfaces.select_config_file"));
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure user selected some interfaces
|
||||
if (this.selectedInterfaces.length === 0) {
|
||||
DialogUtils.alert("Please select at least one interface to import");
|
||||
DialogUtils.alert(this.$t("interfaces.select_at_least_one"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -258,11 +258,9 @@ export default {
|
||||
this.dismiss(true);
|
||||
|
||||
// tell user interfaces were imported
|
||||
DialogUtils.alert(
|
||||
"Interfaces imported successfully. MeshChat must be restarted for these changes to take effect."
|
||||
);
|
||||
DialogUtils.alert(this.$t("interfaces.import_success"));
|
||||
} catch (e) {
|
||||
const message = e.response?.data?.message || "Failed to import interfaces";
|
||||
const message = e.response?.data?.message || this.$t("interfaces.failed_import_all");
|
||||
DialogUtils.alert(message);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@ export default {
|
||||
});
|
||||
this.trackInterfaceChange(interfaceName);
|
||||
} catch (e) {
|
||||
DialogUtils.alert("failed to enable interface");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_enable"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -709,7 +709,7 @@ export default {
|
||||
});
|
||||
this.trackInterfaceChange(interfaceName);
|
||||
} catch (e) {
|
||||
DialogUtils.alert("failed to disable interface");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_disable"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -726,9 +726,7 @@ export default {
|
||||
},
|
||||
async deleteInterface(interfaceName) {
|
||||
// ask user to confirm deleting conversation history
|
||||
if (
|
||||
!(await DialogUtils.confirm("Are you sure you want to delete this interface? This can not be undone!"))
|
||||
) {
|
||||
if (!(await DialogUtils.confirm(this.$t("interfaces.delete_confirm")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -739,7 +737,7 @@ export default {
|
||||
});
|
||||
this.trackInterfaceChange(interfaceName);
|
||||
} catch (e) {
|
||||
DialogUtils.alert("failed to delete interface");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_delete"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -754,7 +752,7 @@ export default {
|
||||
// download file to browser
|
||||
DownloadUtils.downloadFile("meshchat_interfaces.txt", new Blob([response.data]));
|
||||
} catch (e) {
|
||||
DialogUtils.alert("Failed to export interfaces");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_export_all"));
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
@@ -768,7 +766,7 @@ export default {
|
||||
// download file to browser
|
||||
DownloadUtils.downloadFile(`${interfaceName}.txt`, new Blob([response.data]));
|
||||
} catch (e) {
|
||||
DialogUtils.alert("Failed to export interface");
|
||||
DialogUtils.alert(this.$t("interfaces.failed_export_single"));
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
@@ -903,10 +901,10 @@ export default {
|
||||
};
|
||||
|
||||
await window.axios.patch(`/api/v1/reticulum/discovery`, payload);
|
||||
ToastUtils.success("Discovery settings saved");
|
||||
ToastUtils.success(this.$t("interfaces.discovery_settings_saved"));
|
||||
await this.loadDiscoveryConfig();
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to save discovery settings");
|
||||
ToastUtils.error(this.$t("interfaces.failed_save_discovery"));
|
||||
console.log(e);
|
||||
} finally {
|
||||
this.savingDiscovery = false;
|
||||
@@ -962,7 +960,7 @@ export default {
|
||||
GlobalState.modifiedInterfaceNames.clear();
|
||||
await this.loadInterfaces();
|
||||
} catch (e) {
|
||||
ToastUtils.error(e.response?.data?.error || "Failed to reload Reticulum!");
|
||||
ToastUtils.error(e.response?.data?.error || this.$t("interfaces.failed_reload"));
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.reloadingRns = false;
|
||||
|
||||
@@ -10,22 +10,6 @@
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center space-x-2">
|
||||
<!-- export tool toggle -->
|
||||
<button
|
||||
v-if="!offlineEnabled"
|
||||
ref="exportButton"
|
||||
class="p-2 rounded-lg transition-colors"
|
||||
:class="
|
||||
isExportMode
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800'
|
||||
"
|
||||
:title="$t('map.export_area')"
|
||||
@click="toggleExportMode"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="crop-free" class="size-5" />
|
||||
</button>
|
||||
|
||||
<!-- offline/online toggle -->
|
||||
<div class="flex items-center bg-gray-100 dark:bg-zinc-800 rounded-lg p-1">
|
||||
<button
|
||||
@@ -92,6 +76,7 @@
|
||||
<button
|
||||
v-for="tool in drawingTools"
|
||||
:key="tool.type"
|
||||
:ref="tool.type === 'Export' ? 'exportToolButton' : null"
|
||||
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
|
||||
:class="[
|
||||
(drawType === tool.type && !isMeasuring) || (tool.type === 'Export' && isExportMode)
|
||||
@@ -1430,9 +1415,9 @@ export default {
|
||||
await window.axios.post("/api/v1/map/mbtiles/active", { filename });
|
||||
await this.checkOfflineMap();
|
||||
await this.loadMBTilesList();
|
||||
ToastUtils.success("Map source updated");
|
||||
ToastUtils.success(this.$t("map.source_updated"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to set active map");
|
||||
ToastUtils.error(this.$t("map.failed_set_active"));
|
||||
}
|
||||
},
|
||||
async deleteMBTiles(filename) {
|
||||
@@ -1443,9 +1428,9 @@ export default {
|
||||
if (this.metadata && this.metadata.path && this.metadata.path.endsWith(filename)) {
|
||||
await this.checkOfflineMap();
|
||||
}
|
||||
ToastUtils.success("File deleted");
|
||||
ToastUtils.success(this.$t("map.file_deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete file");
|
||||
ToastUtils.error(this.$t("map.failed_delete_file"));
|
||||
}
|
||||
},
|
||||
async saveMBTilesDir() {
|
||||
@@ -1453,10 +1438,10 @@ export default {
|
||||
await window.axios.patch("/api/v1/config", {
|
||||
map_mbtiles_dir: this.mbtilesDir,
|
||||
});
|
||||
ToastUtils.success("Storage directory saved");
|
||||
ToastUtils.success(this.$t("map.storage_saved"));
|
||||
this.loadMBTilesList();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save directory");
|
||||
ToastUtils.error(this.$t("map.failed_save_storage"));
|
||||
}
|
||||
},
|
||||
initMap() {
|
||||
@@ -1937,9 +1922,9 @@ export default {
|
||||
await window.axios.delete(`/api/v1/map/export/${this.exportId}`);
|
||||
this.exportStatus = null;
|
||||
this.exportId = null;
|
||||
ToastUtils.success("Export cancelled");
|
||||
ToastUtils.success(this.$t("map.export_cancelled"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to cancel export");
|
||||
ToastUtils.error(this.$t("map.failed_cancel_export"));
|
||||
}
|
||||
},
|
||||
async startExport() {
|
||||
@@ -1957,7 +1942,7 @@ export default {
|
||||
this.selectedBbox = null;
|
||||
this.pollExportStatus();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to start export");
|
||||
ToastUtils.error(this.$t("map.failed_start_export"));
|
||||
this.isExporting = false;
|
||||
}
|
||||
},
|
||||
@@ -1991,7 +1976,7 @@ export default {
|
||||
if (!file) return;
|
||||
|
||||
if (!file.name.endsWith(".mbtiles")) {
|
||||
ToastUtils.error("Please select an .mbtiles file");
|
||||
ToastUtils.error(this.$t("map.select_mbtiles_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2042,9 +2027,9 @@ export default {
|
||||
map_default_lon: center[0],
|
||||
map_default_zoom: zoom,
|
||||
});
|
||||
ToastUtils.success("Default view saved");
|
||||
ToastUtils.success(this.$t("map.view_saved"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save default view");
|
||||
ToastUtils.error(this.$t("map.failed_save_view"));
|
||||
}
|
||||
},
|
||||
async clearCache() {
|
||||
@@ -2052,7 +2037,7 @@ export default {
|
||||
await TileCache.clear();
|
||||
ToastUtils.success(this.$t("map.cache_cleared"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to clear cache");
|
||||
ToastUtils.error(this.$t("map.failed_clear_cache"));
|
||||
}
|
||||
},
|
||||
async saveTileServerUrl() {
|
||||
@@ -2064,7 +2049,7 @@ export default {
|
||||
ToastUtils.success(this.$t("map.tile_server_saved"));
|
||||
await this.saveMapState();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save tile server URL");
|
||||
ToastUtils.error(this.$t("map.failed_save_tile_server"));
|
||||
}
|
||||
},
|
||||
setTileServer(type) {
|
||||
@@ -2080,7 +2065,7 @@ export default {
|
||||
});
|
||||
ToastUtils.success(this.$t("map.nominatim_api_saved"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save Nominatim API URL");
|
||||
ToastUtils.error(this.$t("map.failed_save_nominatim"));
|
||||
}
|
||||
},
|
||||
checkOnboardingTooltip() {
|
||||
@@ -2094,9 +2079,11 @@ export default {
|
||||
},
|
||||
positionOnboardingTooltip() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.exportButton || !this.$refs.tooltipElement) return;
|
||||
if (!this.$refs.exportToolButton || !this.$refs.tooltipElement) return;
|
||||
|
||||
const exportButton = this.$refs.exportButton;
|
||||
const exportButton = Array.isArray(this.$refs.exportToolButton)
|
||||
? this.$refs.exportToolButton[0]
|
||||
: this.$refs.exportToolButton;
|
||||
const tooltip = this.$refs.tooltipElement;
|
||||
const buttonRect = exportButton.getBoundingClientRect();
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
@@ -2617,7 +2604,7 @@ export default {
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
ToastUtils.success("Copied coordinates");
|
||||
ToastUtils.success(this.$t("map.copied_coordinates"));
|
||||
} else {
|
||||
ToastUtils.success(text);
|
||||
}
|
||||
@@ -2891,7 +2878,7 @@ export default {
|
||||
const response = await window.axios.get("/api/v1/map/drawings");
|
||||
this.savedDrawings = response.data.drawings;
|
||||
} catch {
|
||||
ToastUtils.error("Failed to load drawings");
|
||||
ToastUtils.error(this.$t("map.failed_load_drawings"));
|
||||
} finally {
|
||||
this.isLoadingDrawings = false;
|
||||
}
|
||||
@@ -2900,7 +2887,7 @@ export default {
|
||||
async saveDrawing() {
|
||||
if (!this.newDrawingName.trim()) return;
|
||||
if (!this.drawSource) {
|
||||
ToastUtils.error("Map not initialized");
|
||||
ToastUtils.error(this.$t("map.not_initialized"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2916,11 +2903,11 @@ export default {
|
||||
name: this.newDrawingName,
|
||||
data: json,
|
||||
});
|
||||
ToastUtils.success("Drawing saved");
|
||||
ToastUtils.success(this.$t("map.drawing_saved"));
|
||||
this.showSaveDrawingModal = false;
|
||||
this.newDrawingName = "";
|
||||
} catch {
|
||||
ToastUtils.error("Failed to save drawing");
|
||||
ToastUtils.error(this.$t("map.failed_save_drawing"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2942,9 +2929,9 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/map/drawings/${drawing.id}`);
|
||||
this.savedDrawings = this.savedDrawings.filter((d) => d.id !== drawing.id);
|
||||
ToastUtils.success("Deleted");
|
||||
ToastUtils.success(this.$t("map.deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete");
|
||||
ToastUtils.error(this.$t("map.failed_delete"));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2975,11 +2962,11 @@ export default {
|
||||
},
|
||||
(err) => {
|
||||
console.error("Geolocation failed", err);
|
||||
ToastUtils.warning("Could not determine your location");
|
||||
ToastUtils.warning(this.$t("map.location_not_determined"));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
ToastUtils.warning("Geolocation is not supported by your browser");
|
||||
ToastUtils.warning(this.$t("map.geolocation_not_supported"));
|
||||
}
|
||||
},
|
||||
async fetchTelemetryMarkers() {
|
||||
@@ -3148,7 +3135,7 @@ export default {
|
||||
const nodesWithLoc = discovered.filter((n) => n.latitude != null && n.longitude != null);
|
||||
|
||||
if (nodesWithLoc.length === 0) {
|
||||
ToastUtils.info("No discovered nodes with location found");
|
||||
ToastUtils.info(this.$t("map.no_nodes_location"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3191,7 +3178,7 @@ export default {
|
||||
ToastUtils.success(`Mapped ${nodesWithLoc.length} discovered nodes`);
|
||||
} catch (e) {
|
||||
console.error("Failed to map discovered nodes", e);
|
||||
ToastUtils.error("Failed to fetch discovered nodes for mapping");
|
||||
ToastUtils.error(this.$t("map.failed_fetch_nodes"));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -94,10 +94,10 @@ export default {
|
||||
destination_hash: this.peer.destination_hash,
|
||||
});
|
||||
GlobalEmitter.emit("block-status-changed");
|
||||
DialogUtils.alert("User banished successfully");
|
||||
DialogUtils.alert(this.$t("messages.user_banished"));
|
||||
this.$emit("block-status-changed");
|
||||
} catch (e) {
|
||||
DialogUtils.alert("Failed to banish user");
|
||||
DialogUtils.alert(this.$t("messages.failed_banish_user"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
@@ -105,20 +105,16 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/blocked-destinations/${this.peer.destination_hash}`);
|
||||
GlobalEmitter.emit("block-status-changed");
|
||||
DialogUtils.alert("Banishment lifted successfully");
|
||||
DialogUtils.alert(this.$t("banishment.banishment_lifted"));
|
||||
this.$emit("block-status-changed");
|
||||
} catch (e) {
|
||||
DialogUtils.alert("Failed to lift banishment");
|
||||
DialogUtils.alert(this.$t("banishment.failed_lift_banishment"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
async onDeleteMessageHistory() {
|
||||
// ask user to confirm deleting conversation history
|
||||
if (
|
||||
!(await DialogUtils.confirm(
|
||||
"Are you sure you want to delete all messages in this conversation? This can not be undone!"
|
||||
))
|
||||
) {
|
||||
if (!(await DialogUtils.confirm(this.$t("messages.delete_history_confirm")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,7 +122,7 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/lxmf-messages/conversation/${this.peer.destination_hash}`);
|
||||
} catch (e) {
|
||||
DialogUtils.alert("failed to delete conversation");
|
||||
DialogUtils.alert(this.$t("messages.failed_delete_history"));
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
@@ -138,13 +134,13 @@ export default {
|
||||
},
|
||||
async onPingDestination() {
|
||||
if (!this.peer || !this.peer.destination_hash) {
|
||||
DialogUtils.alert("Invalid destination hash");
|
||||
DialogUtils.alert(this.$t("messages.invalid_destination_hash"));
|
||||
return;
|
||||
}
|
||||
|
||||
const destinationHash = this.peer.destination_hash;
|
||||
if (destinationHash.length !== 32 || !/^[0-9a-fA-F]+$/.test(destinationHash)) {
|
||||
DialogUtils.alert("Invalid destination hash format");
|
||||
DialogUtils.alert(this.$t("messages.invalid_destination_hash_format"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,32 +157,32 @@ export default {
|
||||
const rttDurationString = `${rttMilliseconds} ms`;
|
||||
|
||||
const info = [
|
||||
`Valid reply from ${destinationHash}`,
|
||||
`Duration: ${rttDurationString}`,
|
||||
`Hops There: ${pingResult.hops_there}`,
|
||||
`Hops Back: ${pingResult.hops_back}`,
|
||||
this.$t("messages.ping_reply_from", { hash: destinationHash }),
|
||||
this.$t("messages.duration", { duration: rttDurationString }),
|
||||
this.$t("messages.hops_there", { count: pingResult.hops_there }),
|
||||
this.$t("messages.hops_back", { count: pingResult.hops_back }),
|
||||
];
|
||||
|
||||
// add signal quality if available
|
||||
if (pingResult.quality != null) {
|
||||
info.push(`Signal Quality: ${pingResult.quality}%`);
|
||||
info.push(this.$t("messages.signal_quality", { quality: pingResult.quality }));
|
||||
}
|
||||
|
||||
// add rssi if available
|
||||
if (pingResult.rssi != null) {
|
||||
info.push(`RSSI: ${pingResult.rssi}dBm`);
|
||||
info.push(this.$t("messages.rssi_val", { rssi: pingResult.rssi }));
|
||||
}
|
||||
|
||||
// add snr if available
|
||||
if (pingResult.snr != null) {
|
||||
info.push(`SNR: ${pingResult.snr}dB`);
|
||||
info.push(this.$t("messages.snr_val", { snr: pingResult.snr }));
|
||||
}
|
||||
|
||||
// show result
|
||||
DialogUtils.alert(info.join("\n"));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
const message = e.response?.data?.message ?? "Ping failed. Try again later";
|
||||
const message = e.response?.data?.message ?? this.$t("messages.ping_failed");
|
||||
DialogUtils.alert(message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1681,10 +1681,10 @@ export default {
|
||||
async copyMyAddress() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.myLxmfAddressHash);
|
||||
ToastUtils.success("Your LXMF address copied to clipboard");
|
||||
ToastUtils.success(this.$t("messages.address_copied"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to copy address");
|
||||
ToastUtils.error(this.$t("messages.failed_copy_address"));
|
||||
}
|
||||
},
|
||||
focusComposeInput() {
|
||||
@@ -1737,7 +1737,7 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Translation failed:", e);
|
||||
ToastUtils.error("Translation failed");
|
||||
ToastUtils.error(this.$t("messages.translation_failed"));
|
||||
} finally {
|
||||
this.isSendingMessage = false;
|
||||
}
|
||||
@@ -1949,7 +1949,7 @@ export default {
|
||||
ToastUtils.success(`Added ${name} to contacts`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to add contact");
|
||||
ToastUtils.error(this.$t("messages.failed_add_contact"));
|
||||
}
|
||||
},
|
||||
async ingestPaperMessage(uri) {
|
||||
@@ -1960,10 +1960,10 @@ export default {
|
||||
uri: uri,
|
||||
})
|
||||
);
|
||||
ToastUtils.info("Ingesting paper message...");
|
||||
ToastUtils.info(this.$t("messages.ingesting_paper_message"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to ingest paper message");
|
||||
ToastUtils.error(this.$t("messages.failed_ingest_paper"));
|
||||
}
|
||||
},
|
||||
async generatePaperMessageFromComposition() {
|
||||
@@ -2099,7 +2099,7 @@ export default {
|
||||
destinationHash = destinationHash.replace("lxmf@", "");
|
||||
}
|
||||
if (destinationHash.length !== 32) {
|
||||
DialogUtils.alert("Invalid Address");
|
||||
DialogUtils.alert(this.$t("common.invalid_address"));
|
||||
return;
|
||||
}
|
||||
GlobalEmitter.emit("compose-new-message", destinationHash);
|
||||
@@ -2310,7 +2310,7 @@ export default {
|
||||
}
|
||||
|
||||
// ask user for new display name
|
||||
const displayName = await DialogUtils.prompt("Enter a custom display name");
|
||||
const displayName = await DialogUtils.prompt(this.$t("messages.enter_display_name"));
|
||||
if (displayName == null) {
|
||||
return;
|
||||
}
|
||||
@@ -2331,7 +2331,7 @@ export default {
|
||||
this.$emit("reload-conversations");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
DialogUtils.alert("Failed to update display name");
|
||||
DialogUtils.alert(this.$t("messages.failed_update_display_name"));
|
||||
}
|
||||
},
|
||||
async onConversationDeleted() {
|
||||
@@ -2394,7 +2394,7 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to download or decode audio:", e);
|
||||
DialogUtils.alert("Failed to load audio attachment.");
|
||||
DialogUtils.alert(this.$t("messages.failed_load_audio"));
|
||||
} finally {
|
||||
this.isDownloadingAudio[chatItem.lxmf_message.hash] = false;
|
||||
}
|
||||
@@ -2581,14 +2581,14 @@ export default {
|
||||
this.contacts = response.data;
|
||||
|
||||
if (this.contacts.length === 0) {
|
||||
ToastUtils.info("No contacts found in telephone");
|
||||
ToastUtils.info(this.$t("messages.no_contacts_telephone"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.isShareContactModalOpen = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to load contacts");
|
||||
ToastUtils.error(this.$t("messages.failed_load_contacts"));
|
||||
}
|
||||
},
|
||||
shareContact(contact) {
|
||||
@@ -2838,7 +2838,7 @@ export default {
|
||||
async shareLocation() {
|
||||
try {
|
||||
if (!navigator.geolocation) {
|
||||
DialogUtils.alert("Geolocation is not supported by your browser");
|
||||
DialogUtils.alert(this.$t("map.geolocation_not_supported"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2885,10 +2885,10 @@ export default {
|
||||
},
|
||||
});
|
||||
|
||||
ToastUtils.success("Location request sent");
|
||||
ToastUtils.success(this.$t("messages.location_request_sent"));
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastUtils.error("Failed to send location request");
|
||||
ToastUtils.error(this.$t("messages.failed_send_location_request"));
|
||||
}
|
||||
},
|
||||
viewLocationOnMap(location) {
|
||||
@@ -2911,10 +2911,10 @@ export default {
|
||||
async copyHash(hash) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
ToastUtils.success("Hash copied to clipboard");
|
||||
ToastUtils.success(this.$t("messages.hash_copied"));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to copy hash");
|
||||
ToastUtils.error(this.$t("messages.failed_to_copy_hash"));
|
||||
}
|
||||
},
|
||||
formatBytes: function (bytes) {
|
||||
@@ -2966,7 +2966,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to read clipboard contents: ", err);
|
||||
ToastUtils.error("Failed to read from clipboard");
|
||||
ToastUtils.error(this.$t("messages.failed_read_clipboard"));
|
||||
}
|
||||
},
|
||||
onFileInputChange: function (event) {
|
||||
@@ -2979,7 +2979,7 @@ export default {
|
||||
},
|
||||
async removeImageAttachment(index) {
|
||||
// ask user to confirm removing image attachment
|
||||
if (!(await DialogUtils.confirm("Are you sure you want to remove this image attachment?"))) {
|
||||
if (!(await DialogUtils.confirm(this.$t("messages.remove_image_confirm")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3037,7 +3037,7 @@ export default {
|
||||
|
||||
// alert if failed to start recording
|
||||
if (!this.isRecordingAudioAttachment) {
|
||||
DialogUtils.alert("failed to start recording");
|
||||
DialogUtils.alert(this.$t("messages.failed_start_recording"));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -3059,7 +3059,7 @@ export default {
|
||||
|
||||
// alert if failed to start recording
|
||||
if (!this.isRecordingAudioAttachment) {
|
||||
DialogUtils.alert("failed to start recording");
|
||||
DialogUtils.alert(this.$t("messages.failed_start_recording"));
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -3146,7 +3146,7 @@ export default {
|
||||
},
|
||||
async removeAudioAttachment() {
|
||||
// ask user to confirm removing audio attachment
|
||||
if (!(await DialogUtils.confirm("Are you sure you want to remove this audio attachment?"))) {
|
||||
if (!(await DialogUtils.confirm(this.$t("messages.remove_audio_confirm")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ export default {
|
||||
}
|
||||
|
||||
if (destinationHash.length !== 32) {
|
||||
DialogUtils.alert("Invalid Address");
|
||||
DialogUtils.alert(this.$t("common.invalid_address"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,18 +422,18 @@ export default {
|
||||
try {
|
||||
await window.axios.post("/api/v1/lxmf/folders", { name });
|
||||
await this.getFolders();
|
||||
ToastUtils.success("Folder created");
|
||||
ToastUtils.success(this.$t("messages.folder_created"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to create folder");
|
||||
ToastUtils.error(this.$t("messages.failed_create_folder"));
|
||||
}
|
||||
},
|
||||
async onRenameFolder({ id, name }) {
|
||||
try {
|
||||
await window.axios.patch(`/api/v1/lxmf/folders/${id}`, { name });
|
||||
await this.getFolders();
|
||||
ToastUtils.success("Folder renamed");
|
||||
ToastUtils.success(this.$t("messages.folder_renamed"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to rename folder");
|
||||
ToastUtils.error(this.$t("messages.failed_rename_folder"));
|
||||
}
|
||||
},
|
||||
async onDeleteFolder(id) {
|
||||
@@ -444,9 +444,9 @@ export default {
|
||||
}
|
||||
await this.getFolders();
|
||||
await this.getConversations();
|
||||
ToastUtils.success("Folder deleted");
|
||||
ToastUtils.success(this.$t("messages.folder_deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete folder");
|
||||
ToastUtils.error(this.$t("messages.failed_delete_folder"));
|
||||
}
|
||||
},
|
||||
async onMoveToFolder({ peer_hashes, folder_id }) {
|
||||
@@ -458,9 +458,9 @@ export default {
|
||||
folder_id: targetFolderId,
|
||||
});
|
||||
await this.getConversations();
|
||||
ToastUtils.success("Moved to folder");
|
||||
ToastUtils.success(this.$t("messages.moved_to_folder"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to move to folder");
|
||||
ToastUtils.error(this.$t("messages.failed_move_folder"));
|
||||
}
|
||||
},
|
||||
async onBulkMarkAsRead(destination_hashes) {
|
||||
@@ -469,9 +469,9 @@ export default {
|
||||
destination_hashes,
|
||||
});
|
||||
await this.getConversations();
|
||||
ToastUtils.success("Marked as read");
|
||||
ToastUtils.success(this.$t("messages.marked_read"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to mark as read");
|
||||
ToastUtils.error(this.$t("messages.failed_mark_read"));
|
||||
}
|
||||
},
|
||||
async onBulkDelete(destination_hashes) {
|
||||
@@ -486,9 +486,9 @@ export default {
|
||||
destination_hashes,
|
||||
});
|
||||
await this.getConversations();
|
||||
ToastUtils.success("Conversations deleted");
|
||||
ToastUtils.success(this.$t("messages.conversations_deleted"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to delete conversations");
|
||||
ToastUtils.error(this.$t("messages.failed_delete_conversations"));
|
||||
}
|
||||
},
|
||||
async onExportFolders() {
|
||||
@@ -503,7 +503,7 @@ export default {
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
ToastUtils.error("Failed to export folders");
|
||||
ToastUtils.error(this.$t("messages.failed_export_folders"));
|
||||
}
|
||||
},
|
||||
async onImportFolders() {
|
||||
@@ -520,9 +520,9 @@ export default {
|
||||
await window.axios.post("/api/v1/lxmf/folders/import", data);
|
||||
await this.getFolders();
|
||||
await this.getConversations();
|
||||
ToastUtils.success("Folders imported");
|
||||
ToastUtils.success(this.$t("messages.folders_imported"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to import folders");
|
||||
ToastUtils.error(this.$t("messages.failed_import_folders"));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -641,7 +641,7 @@ export default {
|
||||
try {
|
||||
this.ingestUri = await navigator.clipboard.readText();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to read from clipboard");
|
||||
ToastUtils.error(this.$t("messages.failed_read_clipboard"));
|
||||
}
|
||||
},
|
||||
async ingestPaperMessage() {
|
||||
@@ -656,7 +656,7 @@ export default {
|
||||
);
|
||||
this.isIngestModalOpen = false;
|
||||
} catch {
|
||||
ToastUtils.error("Failed to send ingest request");
|
||||
ToastUtils.error(this.$t("messages.failed_send_ingest"));
|
||||
}
|
||||
},
|
||||
getHashPopoutValue() {
|
||||
|
||||
@@ -175,28 +175,30 @@
|
||||
<!-- search + filters -->
|
||||
<div
|
||||
v-if="conversations.length > 0 || isFilterActive"
|
||||
class="p-1 border-b border-gray-300 dark:border-zinc-700 space-y-2"
|
||||
class="p-1 border-b border-gray-300 dark:border-zinc-700 space-y-1.5"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
:value="conversationSearchTerm"
|
||||
type="text"
|
||||
:placeholder="$t('messages.search_placeholder', { count: conversations.length })"
|
||||
class="input-field flex-1"
|
||||
class="input-field flex-1 w-full rounded-none"
|
||||
@input="onConversationSearchInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-1 px-1 text-gray-500 dark:text-gray-400">
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded-lg hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors"
|
||||
class="p-1 rounded-lg hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
title="Selection Mode"
|
||||
:class="{ 'text-blue-500 bg-blue-50 dark:bg-blue-900/20': selectionMode }"
|
||||
:class="{ 'text-blue-500 dark:text-blue-400': selectionMode }"
|
||||
@click="toggleSelectionMode"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="checkbox-multiple-marked-outline" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded-lg hover:bg-gray-200 dark:hover:bg-zinc-700 transition-colors"
|
||||
class="p-1 rounded-lg hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
||||
title="Ingest Paper Message"
|
||||
@click="openIngestPaperMessageModal"
|
||||
>
|
||||
@@ -866,13 +868,16 @@ export default {
|
||||
this.draggedHash = null;
|
||||
},
|
||||
async createFolder() {
|
||||
const name = await DialogUtils.prompt("Enter folder name", "New Folder");
|
||||
const name = await DialogUtils.prompt(
|
||||
this.$t("messages.enter_folder_name"),
|
||||
this.$t("messages.new_folder")
|
||||
);
|
||||
if (name) {
|
||||
this.$emit("create-folder", name);
|
||||
}
|
||||
},
|
||||
async renameFolder(folder) {
|
||||
const name = await DialogUtils.prompt("Rename folder", folder.name);
|
||||
const name = await DialogUtils.prompt(this.$t("messages.rename_folder"), folder.name);
|
||||
if (name && name !== folder.name) {
|
||||
this.$emit("rename-folder", { id: folder.id, name });
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to render QR code:", err);
|
||||
ToastUtils.error("Failed to render QR code");
|
||||
ToastUtils.error(this.$t("messages.failed_render_qr"));
|
||||
}
|
||||
},
|
||||
close() {
|
||||
@@ -259,9 +259,9 @@ export default {
|
||||
async copyUri() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.uri);
|
||||
ToastUtils.success("URI copied to clipboard");
|
||||
ToastUtils.success(this.$t("messages.uri_copied"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to copy URI");
|
||||
ToastUtils.error(this.$t("messages.failed_copy_uri"));
|
||||
}
|
||||
},
|
||||
downloadQRCode() {
|
||||
@@ -309,14 +309,14 @@ export default {
|
||||
});
|
||||
|
||||
if (response.data.status === "success") {
|
||||
ToastUtils.success("Paper message sent successfully");
|
||||
ToastUtils.success(this.$t("messages.paper_message_sent"));
|
||||
this.close();
|
||||
} else {
|
||||
ToastUtils.error(response.data.message || "Failed to send paper message");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to send paper message:", err);
|
||||
ToastUtils.error("Failed to send paper message");
|
||||
ToastUtils.error(this.$t("messages.failed_send_paper"));
|
||||
} finally {
|
||||
this.isSending = false;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import MicronParser from "micron-parser";
|
||||
import { micronStorage } from "../../js/MicronStorage";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
|
||||
export default {
|
||||
name: "MicronEditorPage",
|
||||
@@ -247,8 +248,8 @@ export default {
|
||||
this.activeTabIndex = this.tabs.length - 1;
|
||||
this.saveContent();
|
||||
},
|
||||
removeTab(index) {
|
||||
if (confirm(this.$t("tools.micron_editor.confirm_delete_tab"))) {
|
||||
async removeTab(index) {
|
||||
if (await DialogUtils.confirm(this.$t("tools.micron_editor.confirm_delete_tab"))) {
|
||||
this.tabs.splice(index, 1);
|
||||
if (this.activeTabIndex >= this.tabs.length) {
|
||||
this.activeTabIndex = Math.max(0, this.tabs.length - 1);
|
||||
@@ -275,7 +276,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async resetAll() {
|
||||
if (confirm(this.$t("tools.micron_editor.confirm_reset"))) {
|
||||
if (await DialogUtils.confirm(this.$t("tools.micron_editor.confirm_reset"))) {
|
||||
await micronStorage.clearAll();
|
||||
this.tabs = [this.createDefaultTab(), this.createGuideTab(Date.now() + 1)];
|
||||
this.activeTabIndex = 0;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
v-if="totalBatches > 0"
|
||||
class="flex justify-between items-center text-[10px] font-bold text-gray-500 dark:text-zinc-500 uppercase tracking-wider"
|
||||
>
|
||||
<span>Batch {{ currentBatch }} / {{ totalBatches }}</span>
|
||||
<span>{{ $t("visualiser.batch") }} {{ currentBatch }} / {{ totalBatches }}</span>
|
||||
<span>{{ Math.round((loadedNodesCount / totalNodesToLoad) * 100) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,12 +53,12 @@
|
||||
@click="isShowingControls = !isShowingControls"
|
||||
>
|
||||
<div class="flex-1 flex flex-col min-w-0 mr-2">
|
||||
<span class="font-bold text-gray-900 dark:text-zinc-100 tracking-tight truncate"
|
||||
>Reticulum Mesh</span
|
||||
>
|
||||
<span class="font-bold text-gray-900 dark:text-zinc-100 tracking-tight truncate">{{
|
||||
$t("visualiser.reticulum_mesh")
|
||||
}}</span>
|
||||
<span
|
||||
class="text-[10px] uppercase font-bold text-gray-500 dark:text-zinc-500 tracking-widest truncate"
|
||||
>Network Visualizer</span
|
||||
>{{ $t("visualiser.network_visualizer") }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -281,7 +281,9 @@ export default {
|
||||
isLoading: false,
|
||||
enablePhysics: true,
|
||||
enableOrbit: false,
|
||||
enableBouncingBalls: false,
|
||||
orbitAnimationFrame: null,
|
||||
bouncingBallsAnimationFrame: null,
|
||||
loadingStatus: "Initializing...",
|
||||
loadedNodesCount: 0,
|
||||
totalNodesToLoad: 0,
|
||||
@@ -323,11 +325,20 @@ export default {
|
||||
},
|
||||
enableOrbit(val) {
|
||||
if (val) {
|
||||
this.enableBouncingBalls = false;
|
||||
this.startOrbit();
|
||||
} else {
|
||||
this.stopOrbit();
|
||||
}
|
||||
},
|
||||
enableBouncingBalls(val) {
|
||||
if (val) {
|
||||
this.enableOrbit = false;
|
||||
this.startBouncingBalls();
|
||||
} else {
|
||||
this.stopBouncingBalls();
|
||||
}
|
||||
},
|
||||
searchQuery() {
|
||||
// we don't want to trigger a full update from server, just re-run the filtering on existing data
|
||||
this.processVisualization();
|
||||
@@ -337,7 +348,11 @@ export default {
|
||||
if (this._toggleOrbitHandler) {
|
||||
GlobalEmitter.off("toggle-orbit", this._toggleOrbitHandler);
|
||||
}
|
||||
if (this._toggleBouncingBallsHandler) {
|
||||
GlobalEmitter.off("toggle-bouncing-balls", this._toggleBouncingBallsHandler);
|
||||
}
|
||||
this.stopOrbit();
|
||||
this.stopBouncingBalls();
|
||||
clearInterval(this.reloadInterval);
|
||||
if (this.network) {
|
||||
this.network.destroy();
|
||||
@@ -359,6 +374,11 @@ export default {
|
||||
};
|
||||
GlobalEmitter.on("toggle-orbit", this._toggleOrbitHandler);
|
||||
|
||||
this._toggleBouncingBallsHandler = () => {
|
||||
this.enableBouncingBalls = !this.enableBouncingBalls;
|
||||
};
|
||||
GlobalEmitter.on("toggle-bouncing-balls", this._toggleBouncingBallsHandler);
|
||||
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
@@ -541,6 +561,7 @@ export default {
|
||||
startOrbit() {
|
||||
if (!this.network) return;
|
||||
this.stopOrbit();
|
||||
this.stopBouncingBalls();
|
||||
|
||||
// Disable physics while orbiting
|
||||
this.network.setOptions({ physics: { enabled: false } });
|
||||
@@ -582,6 +603,19 @@ export default {
|
||||
const mePos = positions["me"] || { x: 0, y: 0 };
|
||||
|
||||
const updates = this._orbitNodes.map((data) => {
|
||||
if (data.id === this._draggingNodeId) {
|
||||
// If dragging, update our internal radius/angle to match new position
|
||||
const nodePositions = this.network.getPositions([data.id]);
|
||||
const pos = nodePositions[data.id];
|
||||
if (pos) {
|
||||
const dx = pos.x - mePos.x;
|
||||
const dy = pos.y - mePos.y;
|
||||
data.radius = Math.sqrt(dx * dx + dy * dy);
|
||||
data.angle = Math.atan2(dy, dx);
|
||||
}
|
||||
return { id: data.id, x: pos.x, y: pos.y };
|
||||
}
|
||||
|
||||
data.angle += data.speed;
|
||||
return {
|
||||
id: data.id,
|
||||
@@ -612,6 +646,127 @@ export default {
|
||||
this.network.setOptions({ physics: { enabled: this.enablePhysics } });
|
||||
}
|
||||
},
|
||||
startBouncingBalls() {
|
||||
if (!this.network) return;
|
||||
this.stopBouncingBalls();
|
||||
this.stopOrbit();
|
||||
|
||||
// Disable physics
|
||||
this.network.setOptions({ physics: { enabled: false } });
|
||||
|
||||
// Hide edges
|
||||
const edges = this.edges.get();
|
||||
const updatedEdges = edges.map((edge) => ({ id: edge.id, hidden: true }));
|
||||
this.edges.update(updatedEdges);
|
||||
|
||||
const container = document.getElementById("network");
|
||||
if (!container) return;
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
const scale = this.network.getScale();
|
||||
const viewPosition = this.network.getViewPosition();
|
||||
|
||||
const halfWidth = width / scale / 2;
|
||||
const halfHeight = height / scale / 2;
|
||||
const topBound = viewPosition.y - halfHeight;
|
||||
const leftBound = viewPosition.x - halfWidth;
|
||||
const rightBound = viewPosition.x + halfWidth;
|
||||
|
||||
const nodeIds = this.nodes.getIds();
|
||||
this._bouncingNodes = nodeIds.map((id) => {
|
||||
const node = this.nodes.get(id);
|
||||
// Get current canvas position if available, otherwise randomize
|
||||
const currentPos = this.network.getPositions([id])[id] || {
|
||||
x: leftBound + Math.random() * (rightBound - leftBound),
|
||||
y: topBound - Math.random() * 800 - 100,
|
||||
};
|
||||
return {
|
||||
id: id,
|
||||
x: currentPos.x,
|
||||
y: currentPos.y < topBound ? currentPos.y : topBound - Math.random() * 800 - 100, // ensure they start above or at their current high pos
|
||||
vx: (Math.random() - 0.5) * 15,
|
||||
vy: Math.random() * 10,
|
||||
radius: (node.size || 25) * 1.5, // approximate collision radius
|
||||
};
|
||||
});
|
||||
|
||||
const gravity = 0.45;
|
||||
const friction = 0.99;
|
||||
const bounce = 0.75;
|
||||
|
||||
const animate = () => {
|
||||
if (!this.enableBouncingBalls) return;
|
||||
|
||||
// Re-calculate boundaries in case of zoom/pan
|
||||
const scale = this.network.getScale();
|
||||
const viewPosition = this.network.getViewPosition();
|
||||
const halfWidth = width / scale / 2;
|
||||
const halfHeight = height / scale / 2;
|
||||
const bottomBound = viewPosition.y + halfHeight;
|
||||
const leftBound = viewPosition.x - halfWidth;
|
||||
const rightBound = viewPosition.x + halfWidth;
|
||||
|
||||
const updates = this._bouncingNodes.map((node) => {
|
||||
if (node.id === this._draggingNodeId) {
|
||||
return {
|
||||
id: node.id,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
};
|
||||
}
|
||||
|
||||
node.vy += gravity;
|
||||
node.vx *= friction;
|
||||
node.vy *= friction;
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
|
||||
// Bounce off bottom
|
||||
if (node.y + node.radius > bottomBound) {
|
||||
node.y = bottomBound - node.radius;
|
||||
node.vy *= -bounce;
|
||||
node.vx += (Math.random() - 0.5) * 4;
|
||||
}
|
||||
|
||||
// Bounce off sides
|
||||
if (node.x - node.radius < leftBound) {
|
||||
node.x = leftBound + node.radius;
|
||||
node.vx *= -bounce;
|
||||
} else if (node.x + node.radius > rightBound) {
|
||||
node.x = rightBound - node.radius;
|
||||
node.vx *= -bounce;
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
};
|
||||
});
|
||||
|
||||
this.nodes.update(updates);
|
||||
this.bouncingBallsAnimationFrame = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
this.bouncingBallsAnimationFrame = requestAnimationFrame(animate);
|
||||
},
|
||||
stopBouncingBalls() {
|
||||
if (this.bouncingBallsAnimationFrame) {
|
||||
cancelAnimationFrame(this.bouncingBallsAnimationFrame);
|
||||
this.bouncingBallsAnimationFrame = null;
|
||||
}
|
||||
|
||||
// Restore edges visibility
|
||||
const edges = this.edges.get();
|
||||
const updatedEdges = edges.map((edge) => ({ id: edge.id, hidden: false }));
|
||||
this.edges.update(updatedEdges);
|
||||
|
||||
// Re-enable physics if it was enabled
|
||||
if (this.network) {
|
||||
this.network.setOptions({ physics: { enabled: this.enablePhysics } });
|
||||
}
|
||||
},
|
||||
async init() {
|
||||
const container = document.getElementById("network");
|
||||
const isDarkMode = document.documentElement.classList.contains("dark");
|
||||
@@ -700,6 +855,36 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on("dragStart", (params) => {
|
||||
if ((this.enableBouncingBalls || this.enableOrbit) && params.nodes.length > 0) {
|
||||
this._draggingNodeId = params.nodes[0];
|
||||
this.network.setOptions({ physics: { enabled: false } });
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on("dragging", (params) => {
|
||||
if (this._draggingNodeId) {
|
||||
const canvasPos = params.pointer.canvas;
|
||||
if (this.enableBouncingBalls) {
|
||||
const node = this._bouncingNodes.find((n) => n.id === this._draggingNodeId);
|
||||
if (node) {
|
||||
node.vx = (canvasPos.x - node.x) * 0.5;
|
||||
node.vy = (canvasPos.y - node.y) * 0.5;
|
||||
node.x = canvasPos.x;
|
||||
node.y = canvasPos.y;
|
||||
}
|
||||
} else if (this.enableOrbit) {
|
||||
// For orbit mode, just update the node position in vis-network DataSet
|
||||
// though it might be overwritten by orbit animation loop
|
||||
this.nodes.update({ id: this._draggingNodeId, x: canvasPos.x, y: canvasPos.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.network.on("dragEnd", () => {
|
||||
this._draggingNodeId = null;
|
||||
});
|
||||
|
||||
await this.manualUpdate();
|
||||
|
||||
// auto reload
|
||||
|
||||
@@ -1230,7 +1230,7 @@ export default {
|
||||
}
|
||||
|
||||
// unsupported url
|
||||
ToastUtils.warning("unsupported url: " + url);
|
||||
ToastUtils.warning(this.$t("nomadnet.unsupported_url") + url);
|
||||
},
|
||||
downloadFileFromBase64: async function (fileName, fileBytesBase64) {
|
||||
// create blob from base64 encoded file bytes
|
||||
@@ -1288,7 +1288,7 @@ export default {
|
||||
await this.getFavourites();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastUtils.error("Failed to rename favourite");
|
||||
ToastUtils.error(this.$t("nomadnet.failed_rename_favourite"));
|
||||
}
|
||||
},
|
||||
async onRemoveFavourite(favourite) {
|
||||
|
||||
@@ -27,85 +27,230 @@
|
||||
v-model="favouritesSearchTerm"
|
||||
type="text"
|
||||
:placeholder="$t('nomadnet.search_favourites_placeholder', { count: favourites.length })"
|
||||
class="input-field"
|
||||
class="input-field w-full rounded-none"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between px-3 pt-2 text-[11px] uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span class="font-semibold">Sections</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 text-xs font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
|
||||
@click="createSection"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="plus" class="size-4" />
|
||||
<span>Add Section</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto px-2 pb-4">
|
||||
<div v-if="searchedFavourites.length > 0" class="space-y-2 pt-2">
|
||||
<div
|
||||
v-for="favourite of searchedFavourites"
|
||||
:key="favourite.destination_hash"
|
||||
class="favourite-card relative"
|
||||
:class="[
|
||||
favourite.destination_hash === selectedDestinationHash ? 'favourite-card--active' : '',
|
||||
draggingFavouriteHash === favourite.destination_hash ? 'favourite-card--dragging' : '',
|
||||
]"
|
||||
draggable="true"
|
||||
@click="onFavouriteClick(favourite)"
|
||||
@dragstart="onFavouriteDragStart($event, favourite)"
|
||||
@dragover.prevent="onFavouriteDragOver($event)"
|
||||
@drop.prevent="onFavouriteDrop($event, favourite)"
|
||||
@dragend="onFavouriteDragEnd"
|
||||
>
|
||||
<!-- banished overlay -->
|
||||
<div
|
||||
v-if="GlobalState.config.banished_effect_enabled && isBlocked(favourite.destination_hash)"
|
||||
class="banished-overlay"
|
||||
:style="{ background: GlobalState.config.banished_color + '33' }"
|
||||
>
|
||||
<span
|
||||
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
|
||||
:style="{ 'background-color': GlobalState.config.banished_color }"
|
||||
>{{ GlobalState.config.banished_text }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="favourite-card__icon flex-shrink-0">
|
||||
<MaterialDesignIcon icon-name="server-network" class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class="text-sm font-semibold text-gray-900 dark:text-white truncate"
|
||||
:title="favourite.display_name"
|
||||
>
|
||||
{{ favourite.display_name }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ formatDestinationHash(favourite.destination_hash) }}
|
||||
</div>
|
||||
</div>
|
||||
<DropDownMenu>
|
||||
<template #button>
|
||||
<IconButton>
|
||||
<MaterialDesignIcon icon-name="dots-vertical" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
</template>
|
||||
<template #items>
|
||||
<DropDownMenuItem @click="onRenameFavourite(favourite)">
|
||||
<MaterialDesignIcon icon-name="pencil" class="w-5 h-5" />
|
||||
<span>{{ $t("nomadnet.rename") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem @click="onRemoveFavourite(favourite)">
|
||||
<MaterialDesignIcon icon-name="trash-can" class="w-5 h-5 text-red-500" />
|
||||
<span class="text-red-500">{{ $t("nomadnet.remove") }}</span>
|
||||
</DropDownMenuItem>
|
||||
<div v-if="isBlocked(favourite.destination_hash)" class="border-t">
|
||||
<DropDownMenuItem @click.stop="onUnblockNode(favourite.destination_hash)">
|
||||
<MaterialDesignIcon icon-name="check-circle" class="w-5 h-5 text-green-500" />
|
||||
<span class="text-green-500">Lift Banishment</span>
|
||||
</DropDownMenuItem>
|
||||
</div>
|
||||
</template>
|
||||
</DropDownMenu>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<div v-if="favourites.length === 0" class="empty-state">
|
||||
<MaterialDesignIcon icon-name="star-outline" class="w-8 h-8" />
|
||||
<div class="font-semibold">{{ $t("nomadnet.no_favourites") }}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ $t("nomadnet.add_nodes_from_announces") }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="hasFavouriteResults" class="space-y-3 pt-2">
|
||||
<div
|
||||
v-for="section in sectionsWithFavourites"
|
||||
:key="section.id"
|
||||
class="rounded-xl"
|
||||
:class="[
|
||||
dragOverSectionId === section.id
|
||||
? 'ring-1 ring-blue-400 dark:ring-blue-600 bg-blue-50/40 dark:bg-blue-900/10'
|
||||
: '',
|
||||
draggingSectionOverId === section.id
|
||||
? 'ring-1 ring-blue-300 dark:ring-blue-700 bg-blue-50/30 dark:bg-blue-900/5'
|
||||
: '',
|
||||
]"
|
||||
@dragover.prevent="onSectionDragOver(section.id)"
|
||||
@dragleave="onSectionDragLeave"
|
||||
@drop.prevent="onDropOnSection(section.id)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-2 py-1 cursor-pointer select-none"
|
||||
draggable="true"
|
||||
@click="toggleSectionCollapse(section.id)"
|
||||
@contextmenu.prevent="openSectionContextMenu($event, section)"
|
||||
@dragstart="onSectionDragStart(section.id)"
|
||||
@dragover.prevent="onSectionReorderDragOver(section.id)"
|
||||
@drop.prevent="onSectionDrop(section.id)"
|
||||
@dragend="onSectionDragEnd"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<MaterialDesignIcon
|
||||
:icon-name="section.collapsed ? 'chevron-right' : 'chevron-down'"
|
||||
class="size-4 text-gray-400"
|
||||
/>
|
||||
<span
|
||||
class="text-xs font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
{{ section.name }}
|
||||
</span>
|
||||
<span
|
||||
v-if="section.collapsed"
|
||||
class="text-[10px] font-semibold text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-zinc-800 px-2 py-0.5 rounded-full"
|
||||
>
|
||||
{{ section.favourites.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1" @click.stop>
|
||||
<button
|
||||
type="button"
|
||||
class="p-1 text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 rounded-lg transition"
|
||||
@click="openSectionContextMenu($event, section)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="dots-vertical" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-px bg-gray-200 dark:bg-zinc-800 mx-1"></div>
|
||||
<div v-if="!section.collapsed" class="space-y-2 pt-2 pb-1 px-1">
|
||||
<div
|
||||
v-for="favourite of section.favourites"
|
||||
:key="favourite.destination_hash"
|
||||
class="favourite-card relative"
|
||||
:class="[
|
||||
favourite.destination_hash === selectedDestinationHash
|
||||
? 'favourite-card--active'
|
||||
: '',
|
||||
draggingFavouriteHash === favourite.destination_hash
|
||||
? 'favourite-card--dragging'
|
||||
: '',
|
||||
]"
|
||||
draggable="true"
|
||||
@click="onFavouriteClick(favourite)"
|
||||
@contextmenu.prevent="openFavouriteContextMenu($event, favourite, section.id)"
|
||||
@dragstart="onFavouriteDragStart($event, favourite, section.id)"
|
||||
@dragover.prevent="onFavouriteDragOver($event)"
|
||||
@drop.prevent="onFavouriteDrop($event, section.id, favourite)"
|
||||
@dragend="onFavouriteDragEnd"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
GlobalState.config.banished_effect_enabled &&
|
||||
isBlocked(favourite.destination_hash)
|
||||
"
|
||||
class="banished-overlay"
|
||||
:style="{ background: GlobalState.config.banished_color + '33' }"
|
||||
>
|
||||
<span
|
||||
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
|
||||
:style="{ 'background-color': GlobalState.config.banished_color }"
|
||||
>{{ GlobalState.config.banished_text }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="favourite-card__icon flex-shrink-0">
|
||||
<MaterialDesignIcon icon-name="server-network" class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
class="text-sm font-semibold text-gray-900 dark:text-white truncate"
|
||||
:title="favourite.display_name"
|
||||
>
|
||||
{{ favourite.display_name }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer inline-flex items-center"
|
||||
:title="$t('common.copy_to_clipboard')"
|
||||
@click.stop="copyToClipboard(favourite.destination_hash, 'Address')"
|
||||
>
|
||||
{{ formatDestinationHash(favourite.destination_hash) }}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
class="text-gray-500 dark:text-gray-300"
|
||||
@click.stop="openFavouriteContextMenu($event, favourite, section.id)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="dots-vertical" class="w-5 h-5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="section.favourites.length === 0"
|
||||
class="text-xs text-gray-500 dark:text-gray-400 px-3 pb-2 italic"
|
||||
>
|
||||
No favourites in this section.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<MaterialDesignIcon icon-name="star-outline" class="w-8 h-8" />
|
||||
<div class="font-semibold">No favourites match your search</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Try a different search term.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favourite Context Menu -->
|
||||
<div
|
||||
v-if="favouriteContextMenu.show"
|
||||
v-click-outside="{ handler: closeContextMenus, capture: true }"
|
||||
class="fixed z-[100] min-w-[220px] bg-white dark:bg-zinc-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-700 py-1.5 overflow-hidden animate-in fade-in zoom-in duration-100"
|
||||
:style="{ top: favouriteContextMenu.y + 'px', left: favouriteContextMenu.x + 'px' }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-700 transition-all active:scale-95"
|
||||
@click="renameFavouriteFromContext"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
|
||||
<span class="font-medium">{{ $t("nomadnet.rename") }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all active:scale-95"
|
||||
@click="removeFavouriteFromContext"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="trash-can" class="size-4 text-red-400" />
|
||||
<span class="font-medium">{{ $t("nomadnet.remove") }}</span>
|
||||
</button>
|
||||
<div class="border-t border-gray-100 dark:border-zinc-700 my-1.5 mx-2"></div>
|
||||
<div
|
||||
class="px-4 py-1.5 text-[10px] font-black text-gray-400 dark:text-zinc-500 uppercase tracking-widest"
|
||||
>
|
||||
Move to Section
|
||||
</div>
|
||||
<div class="max-h-56 overflow-y-auto custom-scrollbar">
|
||||
<button
|
||||
v-for="section in sectionsWithFavourites"
|
||||
:key="section.id + '-move'"
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2 text-sm text-gray-700 dark:text-zinc-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 hover:text-blue-600 dark:hover:text-blue-400 transition-all active:scale-95"
|
||||
@click="moveContextFavouriteToSection(section.id)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="folder" class="size-4 opacity-70" />
|
||||
<span class="truncate">{{ section.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Context Menu -->
|
||||
<div
|
||||
v-if="sectionContextMenu.show"
|
||||
v-click-outside="{ handler: closeContextMenus, capture: true }"
|
||||
class="fixed z-[100] min-w-[200px] bg-white dark:bg-zinc-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-700 py-1.5 overflow-hidden animate-in fade-in zoom-in duration-100"
|
||||
:style="{ top: sectionContextMenu.y + 'px', left: sectionContextMenu.x + 'px' }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 dark:text-zinc-300 hover:bg-gray-100 dark:hover:bg-zinc-700 transition-all active:scale-95"
|
||||
@click="renameSectionFromContext"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
|
||||
<span class="font-medium">Rename Section</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-all active:scale-95"
|
||||
:disabled="sectionContextMenu.sectionId === defaultSectionId"
|
||||
:class="sectionContextMenu.sectionId === defaultSectionId ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
@click="removeSectionFromContext"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="delete" class="size-4 text-red-400" />
|
||||
<span class="font-medium">Delete Section</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +260,7 @@
|
||||
:value="nodesSearchTerm"
|
||||
type="text"
|
||||
:placeholder="$t('nomadnet.search_placeholder_announces', { count: totalNodesCount })"
|
||||
class="input-field"
|
||||
class="input-field w-full rounded-none"
|
||||
@input="onNodesSearchInput"
|
||||
/>
|
||||
</div>
|
||||
@@ -151,8 +296,17 @@
|
||||
>
|
||||
{{ node.display_name }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("nomadnet.announced_time_ago", { time: formatTimeAgo(node.updated_at) }) }}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 flex flex-col gap-0.5">
|
||||
<span class="truncate">{{
|
||||
$t("nomadnet.announced_time_ago", { time: formatTimeAgo(node.updated_at) })
|
||||
}}</span>
|
||||
<span
|
||||
class="cursor-pointer hover:text-blue-500 dark:hover:text-blue-400 inline-flex items-center"
|
||||
:title="$t('common.copy_to_clipboard')"
|
||||
@click.stop="copyToClipboard(node.destination_hash, 'Address')"
|
||||
>
|
||||
{{ formatDestinationHash(node.destination_hash) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,6 +353,7 @@ import DropDownMenuItem from "../DropDownMenuItem.vue";
|
||||
import DialogUtils from "../../js/DialogUtils";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
import GlobalEmitter from "../../js/GlobalEmitter";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
|
||||
export default {
|
||||
name: "NomadNetworkSidebar",
|
||||
@@ -239,8 +394,28 @@ export default {
|
||||
GlobalState,
|
||||
tab: "favourites",
|
||||
favouritesSearchTerm: "",
|
||||
favouritesOrder: [],
|
||||
defaultSectionId: "default",
|
||||
sections: [],
|
||||
sectionOrder: [],
|
||||
favouritesBySection: {},
|
||||
draggingFavouriteHash: null,
|
||||
draggingFavouriteSectionId: null,
|
||||
dragOverSectionId: null,
|
||||
draggingSectionId: null,
|
||||
draggingSectionOverId: null,
|
||||
favouriteContextMenu: {
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
targetHash: null,
|
||||
targetSectionId: null,
|
||||
},
|
||||
sectionContextMenu: {
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
sectionId: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -267,37 +442,155 @@ export default {
|
||||
return matchesDisplayName || matchesDestinationHash;
|
||||
});
|
||||
},
|
||||
orderedFavourites() {
|
||||
return [...this.favourites].sort((a, b) => {
|
||||
return (
|
||||
this.favouritesOrder.indexOf(a.destination_hash) - this.favouritesOrder.indexOf(b.destination_hash)
|
||||
);
|
||||
orderedSections() {
|
||||
const map = {};
|
||||
this.sections.forEach((section) => {
|
||||
map[section.id] = section;
|
||||
});
|
||||
const ids = this.sectionOrder.length > 0 ? this.sectionOrder : this.sections.map((section) => section.id);
|
||||
return ids.map((id) => map[id]).filter((section) => section);
|
||||
},
|
||||
sectionsWithFavourites() {
|
||||
const search = this.favouritesSearchTerm.toLowerCase();
|
||||
return this.orderedSections.map((section) => {
|
||||
const hashes = this.favouritesBySection[section.id] || [];
|
||||
const favourites = hashes
|
||||
.map((hash) => this.favourites.find((fav) => fav.destination_hash === hash))
|
||||
.filter((fav) => fav)
|
||||
.filter((fav) => this.matchesFavouriteSearch(fav, search));
|
||||
return { ...section, favourites };
|
||||
});
|
||||
},
|
||||
searchedFavourites() {
|
||||
return this.orderedFavourites.filter((favourite) => {
|
||||
const search = this.favouritesSearchTerm.toLowerCase();
|
||||
const matchesDisplayName = favourite.display_name.toLowerCase().includes(search);
|
||||
const matchesCustomDisplayName =
|
||||
favourite.custom_display_name?.toLowerCase()?.includes(search) === true;
|
||||
const matchesDestinationHash = favourite.destination_hash.toLowerCase().includes(search);
|
||||
return matchesDisplayName || matchesCustomDisplayName || matchesDestinationHash;
|
||||
});
|
||||
hasFavouriteResults() {
|
||||
if (this.favourites.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (this.favouritesSearchTerm.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
return this.sectionsWithFavourites.some((section) => section.favourites.length > 0);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
favourites: {
|
||||
handler() {
|
||||
this.ensureFavouriteOrder();
|
||||
this.ensureFavouriteLayout();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadFavouriteOrder();
|
||||
this.ensureFavouriteOrder();
|
||||
this.loadFavouriteLayout();
|
||||
this.ensureFavouriteLayout();
|
||||
},
|
||||
methods: {
|
||||
matchesFavouriteSearch(favourite, searchTerm = this.favouritesSearchTerm.toLowerCase()) {
|
||||
const matchesDisplayName = favourite.display_name.toLowerCase().includes(searchTerm);
|
||||
const matchesCustomDisplayName =
|
||||
favourite.custom_display_name?.toLowerCase()?.includes(searchTerm) === true;
|
||||
const matchesDestinationHash = favourite.destination_hash.toLowerCase().includes(searchTerm);
|
||||
return matchesDisplayName || matchesCustomDisplayName || matchesDestinationHash;
|
||||
},
|
||||
buildDefaultSection() {
|
||||
return {
|
||||
id: this.defaultSectionId,
|
||||
name: this.$t("nomadnet.favourites"),
|
||||
collapsed: false,
|
||||
};
|
||||
},
|
||||
resetDefaultSections() {
|
||||
const defaultSection = this.buildDefaultSection();
|
||||
this.sections = [defaultSection];
|
||||
this.sectionOrder = [defaultSection.id];
|
||||
this.favouritesBySection = { [defaultSection.id]: [] };
|
||||
},
|
||||
loadFavouriteLayout() {
|
||||
try {
|
||||
const stored = localStorage.getItem("meshchat.nomadnet.favourites.layout");
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
this.sections = parsed.sections || [];
|
||||
this.sectionOrder =
|
||||
parsed.sectionOrder ||
|
||||
(parsed.sections ? parsed.sections.map((section) => section.id) : this.sectionOrder);
|
||||
this.favouritesBySection = parsed.favouritesBySection || {};
|
||||
return;
|
||||
}
|
||||
const legacyOrder = localStorage.getItem("meshchat.nomadnet.favourites");
|
||||
if (legacyOrder) {
|
||||
const parsedOrder = JSON.parse(legacyOrder);
|
||||
const defaultSection = this.buildDefaultSection();
|
||||
this.sections = [defaultSection];
|
||||
this.sectionOrder = [defaultSection.id];
|
||||
this.favouritesBySection = { [defaultSection.id]: parsedOrder };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
if (this.sections.length === 0) {
|
||||
this.resetDefaultSections();
|
||||
}
|
||||
},
|
||||
persistFavouriteLayout() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"meshchat.nomadnet.favourites.layout",
|
||||
JSON.stringify({
|
||||
sections: this.sections,
|
||||
sectionOrder: this.sectionOrder,
|
||||
favouritesBySection: this.favouritesBySection,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
ensureFavouriteLayout() {
|
||||
if (this.sections.length === 0) {
|
||||
this.resetDefaultSections();
|
||||
}
|
||||
const hashes = this.favourites.map((fav) => fav.destination_hash);
|
||||
const sectionIds = new Set();
|
||||
const sanitizedSections = [];
|
||||
this.sections.forEach((section) => {
|
||||
if (!section || !section.id || sectionIds.has(section.id)) {
|
||||
return;
|
||||
}
|
||||
sectionIds.add(section.id);
|
||||
sanitizedSections.push({
|
||||
id: section.id,
|
||||
name: section.name || this.$t("nomadnet.favourites"),
|
||||
collapsed: section.collapsed === true ? true : false,
|
||||
});
|
||||
});
|
||||
if (!sectionIds.has(this.defaultSectionId)) {
|
||||
const defaultSection = this.buildDefaultSection();
|
||||
sanitizedSections.unshift(defaultSection);
|
||||
sectionIds.add(defaultSection.id);
|
||||
}
|
||||
this.sections = sanitizedSections;
|
||||
const existingOrder = Array.isArray(this.sectionOrder) ? this.sectionOrder : [];
|
||||
const filteredOrder = existingOrder.filter((id) => sectionIds.has(id));
|
||||
const remaining = sanitizedSections
|
||||
.map((section) => section.id)
|
||||
.filter((id) => !filteredOrder.includes(id));
|
||||
this.sectionOrder = [...filteredOrder, ...remaining];
|
||||
|
||||
const nextFavouritesBySection = {};
|
||||
sanitizedSections.forEach((section) => {
|
||||
const existing = this.favouritesBySection[section.id] || [];
|
||||
nextFavouritesBySection[section.id] = existing.filter((hash) => hashes.includes(hash));
|
||||
});
|
||||
const assigned = new Set(Object.values(nextFavouritesBySection).flat());
|
||||
hashes.forEach((hash) => {
|
||||
if (!assigned.has(hash)) {
|
||||
nextFavouritesBySection[this.defaultSectionId].push(hash);
|
||||
assigned.add(hash);
|
||||
}
|
||||
});
|
||||
this.favouritesBySection = nextFavouritesBySection;
|
||||
this.persistFavouriteLayout();
|
||||
},
|
||||
isBlocked(identityHash) {
|
||||
return this.blockedDestinations.some((b) => b.destination_hash === identityHash);
|
||||
},
|
||||
@@ -321,9 +614,9 @@ export default {
|
||||
try {
|
||||
await window.axios.delete(`/api/v1/blocked-destinations/${identityHash}`);
|
||||
GlobalEmitter.emit("block-status-changed");
|
||||
DialogUtils.alert("Banishment lifted successfully");
|
||||
DialogUtils.alert(this.$t("nomadnet.banishment_lifted"));
|
||||
} catch (e) {
|
||||
DialogUtils.alert("Failed to lift banishment");
|
||||
DialogUtils.alert(this.$t("nomadnet.failed_lift_banishment"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
@@ -345,33 +638,7 @@ export default {
|
||||
onRemoveFavourite(favourite) {
|
||||
this.$emit("remove-favourite", favourite);
|
||||
},
|
||||
loadFavouriteOrder() {
|
||||
try {
|
||||
const stored = localStorage.getItem("meshchat.nomadnet.favourites");
|
||||
if (stored) {
|
||||
this.favouritesOrder = JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
persistFavouriteOrder() {
|
||||
localStorage.setItem("meshchat.nomadnet.favourites", JSON.stringify(this.favouritesOrder));
|
||||
},
|
||||
ensureFavouriteOrder() {
|
||||
const hashes = this.favourites.map((fav) => fav.destination_hash);
|
||||
const nextOrder = this.favouritesOrder.filter((hash) => hashes.includes(hash));
|
||||
hashes.forEach((hash) => {
|
||||
if (!nextOrder.includes(hash)) {
|
||||
nextOrder.push(hash);
|
||||
}
|
||||
});
|
||||
if (JSON.stringify(nextOrder) !== JSON.stringify(this.favouritesOrder)) {
|
||||
this.favouritesOrder = nextOrder;
|
||||
this.persistFavouriteOrder();
|
||||
}
|
||||
},
|
||||
onFavouriteDragStart(event, favourite) {
|
||||
onFavouriteDragStart(event, favourite, sectionId) {
|
||||
try {
|
||||
if (event?.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
@@ -381,30 +648,214 @@ export default {
|
||||
// ignore for browsers that prevent setting drag meta
|
||||
}
|
||||
this.draggingFavouriteHash = favourite.destination_hash;
|
||||
this.draggingFavouriteSectionId = sectionId;
|
||||
},
|
||||
onFavouriteDragOver(event) {
|
||||
if (event?.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
},
|
||||
onFavouriteDrop(event, targetFavourite) {
|
||||
onFavouriteDrop(event, targetSectionId, targetFavourite) {
|
||||
if (!this.draggingFavouriteHash || this.draggingFavouriteHash === targetFavourite.destination_hash) {
|
||||
return;
|
||||
}
|
||||
const fromIndex = this.favouritesOrder.indexOf(this.draggingFavouriteHash);
|
||||
const toIndex = this.favouritesOrder.indexOf(targetFavourite.destination_hash);
|
||||
if (fromIndex === -1 || toIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const updated = [...this.favouritesOrder];
|
||||
updated.splice(fromIndex, 1);
|
||||
updated.splice(toIndex, 0, this.draggingFavouriteHash);
|
||||
this.favouritesOrder = updated;
|
||||
this.persistFavouriteOrder();
|
||||
this.draggingFavouriteHash = null;
|
||||
this.moveFavouriteToSection(this.draggingFavouriteHash, targetSectionId, targetFavourite.destination_hash);
|
||||
},
|
||||
onFavouriteDragEnd() {
|
||||
this.draggingFavouriteHash = null;
|
||||
this.draggingFavouriteSectionId = null;
|
||||
this.dragOverSectionId = null;
|
||||
},
|
||||
onSectionDragOver(sectionId) {
|
||||
this.dragOverSectionId = sectionId;
|
||||
},
|
||||
onSectionDragLeave() {
|
||||
this.dragOverSectionId = null;
|
||||
},
|
||||
onDropOnSection(sectionId) {
|
||||
if (!this.draggingFavouriteHash) {
|
||||
return;
|
||||
}
|
||||
this.moveFavouriteToSection(this.draggingFavouriteHash, sectionId);
|
||||
},
|
||||
onSectionDragStart(sectionId) {
|
||||
this.draggingSectionId = sectionId;
|
||||
},
|
||||
onSectionReorderDragOver(sectionId) {
|
||||
if (!this.draggingSectionId || this.draggingSectionId === sectionId) {
|
||||
return;
|
||||
}
|
||||
this.draggingSectionOverId = sectionId;
|
||||
},
|
||||
onSectionDrop(targetSectionId) {
|
||||
if (!this.draggingSectionId || this.draggingSectionId === targetSectionId) {
|
||||
this.onSectionDragEnd();
|
||||
return;
|
||||
}
|
||||
const currentOrder = [...this.sectionOrder];
|
||||
const fromIndex = currentOrder.indexOf(this.draggingSectionId);
|
||||
const toIndex = currentOrder.indexOf(targetSectionId);
|
||||
if (fromIndex === -1 || toIndex === -1) {
|
||||
this.onSectionDragEnd();
|
||||
return;
|
||||
}
|
||||
currentOrder.splice(fromIndex, 1);
|
||||
currentOrder.splice(toIndex, 0, this.draggingSectionId);
|
||||
this.sectionOrder = currentOrder;
|
||||
this.persistFavouriteLayout();
|
||||
this.onSectionDragEnd();
|
||||
},
|
||||
onSectionDragEnd() {
|
||||
this.draggingSectionId = null;
|
||||
this.draggingSectionOverId = null;
|
||||
},
|
||||
moveFavouriteToSection(hash, targetSectionId, beforeHash = null) {
|
||||
if (!hash || !targetSectionId) {
|
||||
return;
|
||||
}
|
||||
const updated = {};
|
||||
Object.keys(this.favouritesBySection).forEach((sectionKey) => {
|
||||
updated[sectionKey] = (this.favouritesBySection[sectionKey] || []).filter((value) => value !== hash);
|
||||
});
|
||||
|
||||
if (!updated[targetSectionId]) {
|
||||
updated[targetSectionId] = [];
|
||||
}
|
||||
const targetList = [...updated[targetSectionId]];
|
||||
if (beforeHash) {
|
||||
const insertIndex = targetList.indexOf(beforeHash);
|
||||
if (insertIndex === -1) {
|
||||
targetList.push(hash);
|
||||
} else {
|
||||
targetList.splice(insertIndex, 0, hash);
|
||||
}
|
||||
} else {
|
||||
targetList.push(hash);
|
||||
}
|
||||
updated[targetSectionId] = targetList;
|
||||
this.favouritesBySection = updated;
|
||||
this.persistFavouriteLayout();
|
||||
this.draggingFavouriteHash = null;
|
||||
this.draggingFavouriteSectionId = null;
|
||||
this.dragOverSectionId = null;
|
||||
},
|
||||
openFavouriteContextMenu(event, favourite, sectionId) {
|
||||
this.favouriteContextMenu = {
|
||||
show: true,
|
||||
x: event.pageX || event.clientX,
|
||||
y: event.pageY || event.clientY,
|
||||
targetHash: favourite.destination_hash,
|
||||
targetSectionId: sectionId,
|
||||
};
|
||||
this.sectionContextMenu.show = false;
|
||||
},
|
||||
openSectionContextMenu(event, section) {
|
||||
this.sectionContextMenu = {
|
||||
show: true,
|
||||
x: event.pageX || event.clientX,
|
||||
y: event.pageY || event.clientY,
|
||||
sectionId: section.id,
|
||||
};
|
||||
this.favouriteContextMenu.show = false;
|
||||
},
|
||||
closeContextMenus() {
|
||||
this.favouriteContextMenu.show = false;
|
||||
this.sectionContextMenu.show = false;
|
||||
},
|
||||
getFavouriteByHash(hash) {
|
||||
return this.favourites.find((fav) => fav.destination_hash === hash);
|
||||
},
|
||||
renameFavouriteFromContext() {
|
||||
const favourite = this.getFavouriteByHash(this.favouriteContextMenu.targetHash);
|
||||
if (!favourite) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
this.closeContextMenus();
|
||||
this.onRenameFavourite(favourite);
|
||||
},
|
||||
removeFavouriteFromContext() {
|
||||
const favourite = this.getFavouriteByHash(this.favouriteContextMenu.targetHash);
|
||||
if (!favourite) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
this.closeContextMenus();
|
||||
this.onRemoveFavourite(favourite);
|
||||
},
|
||||
moveContextFavouriteToSection(sectionId) {
|
||||
if (!this.favouriteContextMenu.targetHash) {
|
||||
return;
|
||||
}
|
||||
this.moveFavouriteToSection(this.favouriteContextMenu.targetHash, sectionId);
|
||||
this.closeContextMenus();
|
||||
},
|
||||
toggleSectionCollapse(sectionId) {
|
||||
const idx = this.sections.findIndex((section) => section.id === sectionId);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
const updated = [...this.sections];
|
||||
const section = { ...updated[idx] };
|
||||
section.collapsed = !section.collapsed;
|
||||
updated[idx] = section;
|
||||
this.sections = updated;
|
||||
this.persistFavouriteLayout();
|
||||
},
|
||||
async createSection() {
|
||||
const name = await DialogUtils.prompt(
|
||||
this.$t("nomadnet.enter_section_name"),
|
||||
this.$t("nomadnet.new_section")
|
||||
);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
const section = {
|
||||
id: `section-${Date.now()}`,
|
||||
name,
|
||||
collapsed: false,
|
||||
};
|
||||
this.sections = [...this.sections, section];
|
||||
this.sectionOrder = [...this.sectionOrder, section.id];
|
||||
this.favouritesBySection = { ...this.favouritesBySection, [section.id]: [] };
|
||||
this.persistFavouriteLayout();
|
||||
},
|
||||
async renameSectionFromContext() {
|
||||
const section = this.sections.find((sec) => sec.id === this.sectionContextMenu.sectionId);
|
||||
if (!section) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
const name = await DialogUtils.prompt(this.$t("nomadnet.rename_section"), section.name);
|
||||
if (!name || name === section.name) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
this.sections = this.sections.map((sec) => (sec.id === section.id ? { ...sec, name } : sec));
|
||||
this.persistFavouriteLayout();
|
||||
this.closeContextMenus();
|
||||
},
|
||||
async removeSectionFromContext() {
|
||||
const sectionId = this.sectionContextMenu.sectionId;
|
||||
if (!sectionId || sectionId === this.defaultSectionId) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
const confirmed = await DialogUtils.confirm(this.$t("nomadnet.delete_section_confirm"));
|
||||
if (!confirmed) {
|
||||
this.closeContextMenus();
|
||||
return;
|
||||
}
|
||||
const retainedSections = this.sections.filter((section) => section.id !== sectionId);
|
||||
const migrated = this.favouritesBySection[sectionId] || [];
|
||||
const nextMap = { ...this.favouritesBySection };
|
||||
delete nextMap[sectionId];
|
||||
nextMap[this.defaultSectionId] = [...(nextMap[this.defaultSectionId] || []), ...migrated];
|
||||
this.sections = retainedSections;
|
||||
this.sectionOrder = this.sectionOrder.filter((id) => id !== sectionId);
|
||||
this.favouritesBySection = nextMap;
|
||||
this.persistFavouriteLayout();
|
||||
this.closeContextMenus();
|
||||
},
|
||||
formatTimeAgo: function (datetimeString) {
|
||||
return Utils.formatTimeAgo(datetimeString);
|
||||
@@ -412,6 +863,11 @@ export default {
|
||||
formatDestinationHash: function (destinationHash) {
|
||||
return Utils.formatDestinationHash(destinationHash);
|
||||
},
|
||||
copyToClipboard(text, label) {
|
||||
if (!text) return;
|
||||
navigator.clipboard.writeText(text);
|
||||
ToastUtils.success(`${label} copied to clipboard`);
|
||||
},
|
||||
onNodesSearchInput(event) {
|
||||
this.$emit("nodes-search-changed", event.target.value);
|
||||
},
|
||||
|
||||
@@ -318,7 +318,7 @@ export default {
|
||||
const response = await window.axios.get("/api/v1/config");
|
||||
this.config = response.data.config;
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to load configuration");
|
||||
ToastUtils.error(this.$t("messages.failed_load_config"));
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
@@ -331,12 +331,12 @@ export default {
|
||||
this.saveOriginalValues();
|
||||
|
||||
if (!silent) {
|
||||
ToastUtils.success("Profile icon saved successfully");
|
||||
ToastUtils.success(this.$t("messages.profile_icon_saved"));
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (!silent) {
|
||||
ToastUtils.error("Failed to save profile icon");
|
||||
ToastUtils.error(this.$t("messages.failed_save_profile_icon"));
|
||||
}
|
||||
console.error(e);
|
||||
return false;
|
||||
@@ -348,12 +348,12 @@ export default {
|
||||
}
|
||||
|
||||
if (!this.iconForegroundColour || !this.iconBackgroundColour) {
|
||||
ToastUtils.warning("Please select both background and icon colors");
|
||||
ToastUtils.warning(this.$t("messages.select_colors_warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.iconName) {
|
||||
ToastUtils.warning("Please select an icon");
|
||||
ToastUtils.warning(this.$t("messages.select_icon_warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ export default {
|
||||
);
|
||||
|
||||
if (success && !silent) {
|
||||
ToastUtils.success("Profile icon saved successfully");
|
||||
ToastUtils.success(this.$t("messages.profile_icon_saved"));
|
||||
}
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
@@ -385,7 +385,7 @@ export default {
|
||||
this.iconForegroundColour = this.originalIconForegroundColour;
|
||||
this.iconBackgroundColour = this.originalIconBackgroundColour;
|
||||
|
||||
ToastUtils.info("Changes reset to saved values");
|
||||
ToastUtils.info(this.$t("messages.changes_reset"));
|
||||
},
|
||||
onIconClick(iconName) {
|
||||
this.iconName = iconName;
|
||||
@@ -401,7 +401,7 @@ export default {
|
||||
});
|
||||
|
||||
if (success) {
|
||||
ToastUtils.success("Profile icon removed successfully");
|
||||
ToastUtils.success(this.$t("messages.profile_icon_removed"));
|
||||
}
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
|
||||
@@ -374,7 +374,7 @@ export default {
|
||||
const response = await window.axios.patch("/api/v1/config", config);
|
||||
this.config = response.data.config;
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to save config!");
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -242,12 +242,12 @@ export default {
|
||||
this.identities = response.data.identities;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to load identities");
|
||||
ToastUtils.error(this.$t("identities.failed_load"));
|
||||
}
|
||||
},
|
||||
async createIdentity() {
|
||||
if (!this.newIdentityName) {
|
||||
ToastUtils.warning("Please enter a display name");
|
||||
ToastUtils.warning(this.$t("identities.enter_display_name_warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -256,13 +256,13 @@ export default {
|
||||
await window.axios.post("/api/v1/identities/create", {
|
||||
display_name: this.newIdentityName,
|
||||
});
|
||||
ToastUtils.success("Identity created successfully");
|
||||
ToastUtils.success(this.$t("identities.created"));
|
||||
this.showCreateModal = false;
|
||||
this.newIdentityName = "";
|
||||
await this.getIdentities();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to create identity");
|
||||
ToastUtils.error(this.$t("identities.failed_create"));
|
||||
} finally {
|
||||
this.isCreating = false;
|
||||
}
|
||||
@@ -286,14 +286,15 @@ export default {
|
||||
// Success is handled by GlobalEmitter "identity-switched" which we listen to
|
||||
ToastUtils.success(this.$t("identities.switched") || "Identity switched successfully");
|
||||
} else {
|
||||
ToastUtils.info("Switch scheduled. Reloading application...");
|
||||
ToastUtils.info(this.$t("identities.switch_scheduled"));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const errorMsg = e.response?.data?.message || "Failed to switch identity";
|
||||
const errorMsg =
|
||||
e.response?.data?.message || this.$t("identities.failed_switch") || "Failed to switch identity";
|
||||
ToastUtils.error(errorMsg);
|
||||
this.isCreating = false;
|
||||
|
||||
@@ -313,7 +314,7 @@ export default {
|
||||
await this.getIdentities();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to delete identity");
|
||||
ToastUtils.error(this.$t("identities.failed_delete"));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -105,10 +105,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- search bar -->
|
||||
<div class="sticky top-0 z-10 py-4">
|
||||
<div class="relative max-w-6xl mx-auto">
|
||||
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="w-full bg-white/80 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-800 rounded-2xl py-3 pl-12 pr-4 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-none transition-all shadow-sm"
|
||||
:placeholder="$t('app.search_settings') || 'Search settings...'"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery"
|
||||
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
@click="searchQuery = ''"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="close-circle" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- no results -->
|
||||
<div
|
||||
v-if="searchQuery && !hasSearchResults"
|
||||
class="flex flex-col items-center justify-center py-12 text-center"
|
||||
>
|
||||
<div
|
||||
class="p-4 bg-white/50 dark:bg-zinc-800/50 rounded-full mb-4 border border-gray-100 dark:border-zinc-800"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="magnify-close" class="size-8 text-gray-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">No results found</h3>
|
||||
<p class="text-gray-500 dark:text-gray-400">No settings match "{{ searchQuery }}"</p>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition font-semibold text-sm"
|
||||
@click="searchQuery = ''"
|
||||
>
|
||||
Clear search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- settings grid -->
|
||||
<div class="columns-1 lg:columns-2 gap-4 space-y-4">
|
||||
<div v-show="hasSearchResults" class="columns-1 lg:columns-2 gap-4 space-y-4">
|
||||
<!-- Banishment -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.banishment)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Visuals</div>
|
||||
@@ -174,7 +219,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Maintenance & Data -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.maintenance)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Maintenance</div>
|
||||
@@ -335,7 +383,11 @@
|
||||
</section>
|
||||
|
||||
<!-- Desktop / Electron Settings -->
|
||||
<section v-if="ElectronUtils.isElectron()" class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-if="ElectronUtils.isElectron()"
|
||||
v-show="matchesSearch(...sectionKeywords.desktop)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Desktop</div>
|
||||
@@ -381,7 +433,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Page Archiver -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section v-show="matchesSearch(...sectionKeywords.archiver)" class="glass-card break-inside-avoid">
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Browsing</div>
|
||||
@@ -448,7 +500,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Smart Crawler -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section v-show="matchesSearch(...sectionKeywords.crawler)" class="glass-card break-inside-avoid">
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Discovery</div>
|
||||
@@ -523,7 +575,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Appearance -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.appearance)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Personalise</div>
|
||||
@@ -581,7 +636,19 @@
|
||||
</section>
|
||||
|
||||
<!-- Language -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="
|
||||
matchesSearch(
|
||||
'i18n',
|
||||
'app.language',
|
||||
'app.select_language',
|
||||
'English',
|
||||
'Deutsch',
|
||||
'Русский'
|
||||
)
|
||||
"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">i18n</div>
|
||||
@@ -599,7 +666,17 @@
|
||||
</section>
|
||||
|
||||
<!-- Network Security -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="
|
||||
matchesSearch(
|
||||
'RNS Security',
|
||||
'Network Security',
|
||||
'app.blackhole_integration_enabled',
|
||||
'app.blackhole_integration_description'
|
||||
)
|
||||
"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">RNS Security</div>
|
||||
@@ -633,7 +710,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Transport -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section v-show="matchesSearch(...sectionKeywords.transport)" class="glass-card break-inside-avoid">
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Reticulum</div>
|
||||
@@ -660,7 +737,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Interfaces -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.interfaces)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Adapters</div>
|
||||
@@ -686,7 +766,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Blocked -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch('Privacy', 'Banished', 'Manage Banished users and nodes')"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Privacy</div>
|
||||
@@ -704,7 +787,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Authentication -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section v-show="matchesSearch(...sectionKeywords.auth)" class="glass-card break-inside-avoid">
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Security</div>
|
||||
@@ -736,7 +819,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Translator -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.translator)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">i18n</div>
|
||||
@@ -778,7 +864,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Sources & Infrastructure -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.infrastructure)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">Infrastructure</div>
|
||||
@@ -819,7 +908,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Messages -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section v-show="matchesSearch(...sectionKeywords.messages)" class="glass-card break-inside-avoid">
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">{{ $t("app.reliability") }}</div>
|
||||
@@ -888,7 +977,10 @@
|
||||
</section>
|
||||
|
||||
<!-- Propagation nodes -->
|
||||
<section class="glass-card break-inside-avoid">
|
||||
<section
|
||||
v-show="matchesSearch(...sectionKeywords.propagation)"
|
||||
class="glass-card break-inside-avoid"
|
||||
>
|
||||
<header class="glass-card__header">
|
||||
<div>
|
||||
<div class="glass-card__eyebrow">LXMF</div>
|
||||
@@ -1021,7 +1113,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts (Full width at bottom) -->
|
||||
<div class="mt-4">
|
||||
<div v-show="matchesSearch(...sectionKeywords.shortcuts)" class="mt-4">
|
||||
<section class="glass-card">
|
||||
<div class="glass-card__header">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -1113,9 +1205,126 @@ export default {
|
||||
saveTimeouts: {},
|
||||
shortcuts: [],
|
||||
reloadingRns: false,
|
||||
searchQuery: "",
|
||||
sectionKeywords: {
|
||||
banishment: [
|
||||
"Visuals",
|
||||
"app.banishment",
|
||||
"app.banishment_description",
|
||||
"app.banished_effect_enabled",
|
||||
"app.banished_effect_description",
|
||||
"app.banished_text_label",
|
||||
"app.banished_text_description",
|
||||
"app.banished_color_label",
|
||||
"app.banished_color_description",
|
||||
],
|
||||
maintenance: [
|
||||
"Maintenance",
|
||||
"maintenance.title",
|
||||
"maintenance.description",
|
||||
"maintenance.clear_messages",
|
||||
"maintenance.clear_messages_desc",
|
||||
"maintenance.clear_announces",
|
||||
"maintenance.clear_announces_desc",
|
||||
"maintenance.clear_nomadnet_favs",
|
||||
"maintenance.clear_nomadnet_favs_desc",
|
||||
"maintenance.clear_archives",
|
||||
"maintenance.clear_archives_desc",
|
||||
"maintenance.export_messages",
|
||||
"maintenance.export_messages_desc",
|
||||
"maintenance.import_messages",
|
||||
"maintenance.import_messages_desc",
|
||||
"Automatic Backup Limit",
|
||||
"Export Folders",
|
||||
"Import Folders",
|
||||
],
|
||||
desktop: [
|
||||
"Desktop",
|
||||
"App Behaviour",
|
||||
"app.desktop_open_calls_in_separate_window",
|
||||
"app.desktop_open_calls_in_separate_window_description",
|
||||
"app.desktop_hardware_acceleration_enabled",
|
||||
"app.desktop_hardware_acceleration_enabled_description",
|
||||
],
|
||||
archiver: ["Browsing", "Page Archiver", "archiver", "archive", "versions", "storage", "flush"],
|
||||
crawler: ["Discovery", "Smart Crawler", "crawler", "crawl", "retries", "delay", "concurrent"],
|
||||
appearance: [
|
||||
"Personalise",
|
||||
"app.appearance",
|
||||
"app.appearance_description",
|
||||
"app.theme",
|
||||
"app.light_theme",
|
||||
"app.dark_theme",
|
||||
"Message Font Size",
|
||||
"app.live_preview",
|
||||
"app.realtime",
|
||||
],
|
||||
language: ["i18n", "app.language", "app.select_language", "English", "Deutsch", "Русский"],
|
||||
networkSecurity: [
|
||||
"RNS Security",
|
||||
"Network Security",
|
||||
"app.blackhole_integration_enabled",
|
||||
"app.blackhole_integration_description",
|
||||
],
|
||||
transport: [
|
||||
"Reticulum",
|
||||
"app.transport_mode",
|
||||
"app.transport_description",
|
||||
"app.enable_transport_mode",
|
||||
"app.transport_toggle_description",
|
||||
],
|
||||
interfaces: [
|
||||
"Adapters",
|
||||
"app.interfaces",
|
||||
"app.show_community_interfaces",
|
||||
"app.community_interfaces_description",
|
||||
],
|
||||
blocked: ["Privacy", "Banished", "Manage Banished users and nodes"],
|
||||
auth: ["Security", "Authentication", "password", "Protect your instance with a password"],
|
||||
translator: [
|
||||
"i18n",
|
||||
"app.translator",
|
||||
"translator.description",
|
||||
"app.translator_enabled",
|
||||
"app.translator_description",
|
||||
"app.libretranslate_url",
|
||||
"app.libretranslate_url_description",
|
||||
],
|
||||
infrastructure: ["Infrastructure", "Sources & Mirroring", "gitea", "documentation", "download", "urls"],
|
||||
messages: [
|
||||
"reliability",
|
||||
"app.messages",
|
||||
"app.messages_description",
|
||||
"app.auto_resend_title",
|
||||
"app.auto_resend_description",
|
||||
"app.retry_attachments_title",
|
||||
"app.retry_attachments_description",
|
||||
"app.auto_fallback_title",
|
||||
"app.auto_fallback_description",
|
||||
"app.inbound_stamp_cost",
|
||||
"app.inbound_stamp_description",
|
||||
],
|
||||
propagation: [
|
||||
"LXMF",
|
||||
"app.propagation_nodes",
|
||||
"app.propagation_nodes_description",
|
||||
"app.browse_nodes",
|
||||
"app.run_local_node",
|
||||
"app.run_local_node_description",
|
||||
"app.preferred_propagation_node",
|
||||
"app.auto_sync_interval",
|
||||
"app.propagation_stamp_cost",
|
||||
"app.propagation_stamp_description",
|
||||
],
|
||||
shortcuts: ["Keyboard Shortcuts", "actions", "workflow"],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasSearchResults() {
|
||||
if (!this.searchQuery) return true;
|
||||
return Object.values(this.sectionKeywords).some((keywords) => this.matchesSearch(...keywords));
|
||||
},
|
||||
safeConfig() {
|
||||
if (!this.config) {
|
||||
return {
|
||||
@@ -1140,6 +1349,16 @@ export default {
|
||||
this.getConfig();
|
||||
},
|
||||
methods: {
|
||||
matchesSearch(...texts) {
|
||||
if (!this.searchQuery) return true;
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
return texts.some((text) => {
|
||||
if (!text) return false;
|
||||
// If it looks like a translation key, translate it
|
||||
const content = text.includes(".") ? this.$t(text) : text;
|
||||
return content.toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
async onWebsocketMessage(message) {
|
||||
const json = JSON.parse(message.data);
|
||||
switch (json.type) {
|
||||
@@ -1184,11 +1403,11 @@ export default {
|
||||
},
|
||||
async saveShortcut(action, keys) {
|
||||
await KeyboardShortcuts.saveShortcut(action, keys);
|
||||
ToastUtils.success("Shortcut saved");
|
||||
ToastUtils.success(this.$t("settings.shortcut_saved"));
|
||||
},
|
||||
async deleteShortcut(action) {
|
||||
await KeyboardShortcuts.deleteShortcut(action);
|
||||
ToastUtils.success("Shortcut deleted");
|
||||
ToastUtils.success(this.$t("settings.shortcut_deleted"));
|
||||
},
|
||||
async updateConfig(config, label = null) {
|
||||
try {
|
||||
@@ -1198,7 +1417,7 @@ export default {
|
||||
ToastUtils.success(this.$t("app.setting_auto_saved", { label: this.$t(`app.${label}`) }));
|
||||
}
|
||||
} catch (e) {
|
||||
ToastUtils.error("Failed to save config!");
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
@@ -1498,7 +1717,7 @@ export default {
|
||||
type: "nomadnet.page.archive.flush",
|
||||
})
|
||||
);
|
||||
ToastUtils.success("Archived pages flushed.");
|
||||
ToastUtils.success(this.$t("settings.archived_pages_flushed"));
|
||||
},
|
||||
async onIsTransportEnabledChangeWrapper(value) {
|
||||
this.config.is_transport_enabled = value;
|
||||
@@ -1509,13 +1728,13 @@ export default {
|
||||
try {
|
||||
await window.axios.post("/api/v1/reticulum/enable-transport");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to enable transport mode!");
|
||||
ToastUtils.error(this.$t("settings.failed_enable_transport"));
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await window.axios.post("/api/v1/reticulum/disable-transport");
|
||||
} catch {
|
||||
ToastUtils.error("Failed to disable transport mode!");
|
||||
ToastUtils.error(this.$t("settings.failed_disable_transport"));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1527,7 +1746,7 @@ export default {
|
||||
const response = await window.axios.post("/api/v1/reticulum/reload");
|
||||
ToastUtils.success(response.data.message);
|
||||
} catch {
|
||||
ToastUtils.error("Failed to reload Reticulum!");
|
||||
ToastUtils.error(this.$t("settings.failed_reload_reticulum"));
|
||||
} finally {
|
||||
this.reloadingRns = false;
|
||||
}
|
||||
@@ -1622,9 +1841,9 @@ export default {
|
||||
linkElement.setAttribute("href", dataUri);
|
||||
linkElement.setAttribute("download", exportFileDefaultName);
|
||||
linkElement.click();
|
||||
ToastUtils.success("Folders exported");
|
||||
ToastUtils.success(this.$t("settings.folders_exported"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to export folders");
|
||||
ToastUtils.error(this.$t("settings.failed_export_folders"));
|
||||
}
|
||||
},
|
||||
triggerFolderImport() {
|
||||
@@ -1641,9 +1860,9 @@ export default {
|
||||
if (!data.folders || !data.mappings) throw new Error("Invalid file format");
|
||||
|
||||
await window.axios.post("/api/v1/lxmf/folders/import", data);
|
||||
ToastUtils.success("Folders and mappings imported successfully");
|
||||
ToastUtils.success(this.$t("settings.folders_imported"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to import folders");
|
||||
ToastUtils.error(this.$t("settings.failed_import_folders"));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
@@ -313,7 +313,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to render QR code:", err);
|
||||
ToastUtils.error("Failed to render QR code");
|
||||
ToastUtils.error(this.$t("messages.failed_render_qr"));
|
||||
}
|
||||
},
|
||||
async ingestPaperMessage() {
|
||||
@@ -330,15 +330,15 @@ export default {
|
||||
try {
|
||||
this.ingestUri = await navigator.clipboard.readText();
|
||||
} catch {
|
||||
ToastUtils.error("Failed to read from clipboard");
|
||||
ToastUtils.error(this.$t("messages.failed_read_clipboard"));
|
||||
}
|
||||
},
|
||||
async copyUri() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.generatedUri);
|
||||
ToastUtils.success("URI copied to clipboard");
|
||||
ToastUtils.success(this.$t("messages.uri_copied"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to copy URI");
|
||||
ToastUtils.error(this.$t("messages.failed_copy_uri"));
|
||||
}
|
||||
},
|
||||
downloadQRCode() {
|
||||
@@ -386,7 +386,7 @@ export default {
|
||||
});
|
||||
|
||||
if (response.data.status === "success") {
|
||||
ToastUtils.success("Paper message sent successfully");
|
||||
ToastUtils.success(this.$t("messages.paper_message_sent"));
|
||||
this.generatedUri = null;
|
||||
this.destinationHash = "";
|
||||
this.content = "";
|
||||
@@ -396,7 +396,7 @@ export default {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to send paper message:", err);
|
||||
ToastUtils.error("Failed to send paper message");
|
||||
ToastUtils.error(this.$t("messages.failed_send_paper"));
|
||||
} finally {
|
||||
this.isSending = false;
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ export default {
|
||||
this.interfaces = Object.keys(ifaceRes.data.interfaces);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to fetch path data");
|
||||
ToastUtils.error(this.$t("tools.rnpath.failed_fetch"));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@@ -421,13 +421,13 @@ export default {
|
||||
try {
|
||||
const res = await window.axios.post("/api/v1/rnpath/drop", { destination_hash: hash });
|
||||
if (res.data.success) {
|
||||
ToastUtils.success("Path dropped");
|
||||
ToastUtils.success(this.$t("tools.rnpath.path_dropped"));
|
||||
this.refreshAll();
|
||||
} else {
|
||||
ToastUtils.error("Could not drop path");
|
||||
ToastUtils.error(this.$t("tools.rnpath.failed_drop"));
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error("Error dropping path");
|
||||
ToastUtils.error(this.$t("tools.rnpath.error_drop"));
|
||||
}
|
||||
},
|
||||
async requestPath() {
|
||||
@@ -437,7 +437,7 @@ export default {
|
||||
this.requestHash = "";
|
||||
// Path requests take time, don't refresh immediately
|
||||
} catch {
|
||||
ToastUtils.error("Failed to request path");
|
||||
ToastUtils.error(this.$t("tools.rnpath.failed_request"));
|
||||
}
|
||||
},
|
||||
async dropAllVia() {
|
||||
@@ -449,23 +449,23 @@ export default {
|
||||
transport_instance_hash: this.dropViaHash,
|
||||
});
|
||||
if (res.data.success) {
|
||||
ToastUtils.success("Paths dropped");
|
||||
ToastUtils.success(this.$t("tools.rnpath.paths_dropped"));
|
||||
this.dropViaHash = "";
|
||||
this.refreshAll();
|
||||
}
|
||||
} catch {
|
||||
ToastUtils.error("Failed to drop paths");
|
||||
ToastUtils.error(this.$t("tools.rnpath.failed_drop_paths"));
|
||||
}
|
||||
},
|
||||
async dropAnnounceQueues() {
|
||||
if (!(await DialogUtils.confirm("Purge all announce queues? This cannot be undone."))) {
|
||||
if (!(await DialogUtils.confirm(this.$t("tools.rnpath.purge_confirm")))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await window.axios.post("/api/v1/rnpath/drop-queues");
|
||||
ToastUtils.success("Announce queues purged");
|
||||
ToastUtils.success(this.$t("tools.rnpath.queues_purged"));
|
||||
} catch {
|
||||
ToastUtils.error("Failed to purge queues");
|
||||
ToastUtils.error(this.$t("tools.rnpath.failed_purge"));
|
||||
}
|
||||
},
|
||||
calculateRate(rate) {
|
||||
|
||||
@@ -453,14 +453,12 @@ export default {
|
||||
this.hasArgos = response.data.has_argos;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
DialogUtils.alert(
|
||||
"Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed."
|
||||
);
|
||||
DialogUtils.alert(this.$t("translator.failed_load_languages"));
|
||||
}
|
||||
},
|
||||
copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text);
|
||||
ToastUtils.success("Copied to clipboard");
|
||||
ToastUtils.success(this.$t("common.copied"));
|
||||
},
|
||||
async translateText() {
|
||||
if (!this.canTranslate || this.isTranslating) {
|
||||
@@ -468,12 +466,12 @@ export default {
|
||||
}
|
||||
|
||||
if (!this.sourceLang || !this.targetLang) {
|
||||
this.error = "Please select both source and target languages.";
|
||||
this.error = this.$t("translator.select_languages_warning");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.translationMode === "argos" && this.sourceLang === "auto") {
|
||||
this.error = "Auto-detection is not supported with Argos Translate. Please select a source language.";
|
||||
this.error = this.$t("translator.auto_detect_not_supported");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -499,9 +497,7 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error =
|
||||
e.response?.data?.message ||
|
||||
"Translation failed. Make sure LibreTranslate is running or Argos Translate is installed.";
|
||||
this.error = e.response?.data?.message || this.$t("translator.failed_translate");
|
||||
} finally {
|
||||
this.isTranslating = false;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,10 @@
|
||||
"loading_identity": "Ihre Identität wird geladen",
|
||||
"emergency_mode_active": "Notfallmodus aktiv - In-Memory-Datenbank und eingeschränkte Dienste werden verwendet.",
|
||||
"blackhole_integration_enabled": "Blackhole-Integration",
|
||||
"blackhole_integration_description": "Identitäten automatisch auf der Reticulum-Transportebene sperren (Blackhole), wenn Benutzer in MeshChatX verbannt werden."
|
||||
"blackhole_integration_description": "Identitäten automatisch auf der Reticulum-Transportebene sperren (Blackhole), wenn Benutzer in MeshChatX verbannt werden.",
|
||||
"failed_announce": "failed to announce",
|
||||
"search_settings": "Search settings...",
|
||||
"show_qr": "Show QR Code"
|
||||
},
|
||||
"common": {
|
||||
"open": "Öffnen",
|
||||
@@ -213,7 +216,19 @@
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie dies löschen möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"search": "Werkzeuge suchen...",
|
||||
"no_results": "Keine Werkzeuge gefunden",
|
||||
"error": "Fehler"
|
||||
"error": "Fehler",
|
||||
"continue": "Continue",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"copied": "Copied to clipboard",
|
||||
"failed_to_copy": "Failed to copy to clipboard",
|
||||
"save_failed": "Failed to save configuration!",
|
||||
"invalid_address": "Invalid Address",
|
||||
"loading": "Loading...",
|
||||
"ok": "OK",
|
||||
"copy": "Copy",
|
||||
"copy_to_clipboard": "Copy to clipboard"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Wartung & Daten",
|
||||
@@ -254,7 +269,13 @@
|
||||
"delete_confirm": "Sind Sie sicher, dass Sie die Identität \"{name}\" löschen möchten? Dies kann nicht rückgängig gemacht werden.",
|
||||
"switched": "Identität gewechselt. Die Anwendung wird jetzt neu gestartet, um die Änderungen zu übernehmen.",
|
||||
"created": "Identität erfolgreich erstellt",
|
||||
"deleted": "Identität gelöscht"
|
||||
"deleted": "Identität gelöscht",
|
||||
"failed_load": "Failed to load identities",
|
||||
"enter_display_name_warning": "Please enter a display name",
|
||||
"failed_create": "Failed to create identity",
|
||||
"switch_scheduled": "Switch scheduled. Reloading application...",
|
||||
"failed_switch": "Failed to switch identity",
|
||||
"failed_delete": "Failed to delete identity"
|
||||
},
|
||||
"about": {
|
||||
"title": "Über",
|
||||
@@ -305,7 +326,26 @@
|
||||
"no_integrity_violations": "Keine Integritätsverletzungen seit dem letzten Start erkannt.",
|
||||
"dependency_chain": "Abhängigkeitskette",
|
||||
"other_core_components": "Andere Kernkomponenten",
|
||||
"backend_dependencies": "Backend-Abhängigkeiten"
|
||||
"backend_dependencies": "Backend-Abhängigkeiten",
|
||||
"integrity_backend_error": "The application backend binary (unpacked from ASAR) appears to have been modified or replaced. This could indicate a malicious actor trying to compromise your mesh communication.",
|
||||
"integrity_data_error": "Your identities or database files appear to have been modified while the app was closed.",
|
||||
"integrity_warning_footer": "Proceed with caution. If you did not manually update or modify these files, your installation may be compromised.",
|
||||
"delete_snapshot_confirm": "Are you sure you want to delete this snapshot?",
|
||||
"snapshot_deleted": "Snapshot deleted",
|
||||
"failed_delete_snapshot": "Failed to delete snapshot",
|
||||
"delete_backup_confirm": "Are you sure you want to delete this backup?",
|
||||
"backup_deleted": "Backup deleted",
|
||||
"failed_delete_backup": "Failed to delete backup",
|
||||
"database_restored": "Database restored. Relaunching...",
|
||||
"failed_restore_snapshot": "Failed to restore snapshot",
|
||||
"restore_snapshot_confirm": "Are you sure you want to restore this snapshot? This will overwrite the current database and require an app relaunch.",
|
||||
"integrity_acknowledged": "Integrity issues acknowledged",
|
||||
"integrity_acknowledged_reset": "Integrity issues acknowledged and manifest reset",
|
||||
"integrity_acknowledge_confirm": "Are you sure you want to acknowledge these integrity issues? This will update the security manifest to match the current state of your files.",
|
||||
"failed_acknowledge_integrity": "Failed to acknowledge integrity issues",
|
||||
"shutdown_sent": "Shutdown command sent to server.",
|
||||
"identity_exported": "Identity key file exported",
|
||||
"identity_copied": "Identity Base32 key copied to clipboard"
|
||||
},
|
||||
"interfaces": {
|
||||
"title": "Schnittstellen",
|
||||
@@ -321,7 +361,23 @@
|
||||
"no_interfaces_description": "Passen Sie Ihre Suche an oder fügen Sie eine neue Schnittstelle hinzu.",
|
||||
"restart_required": "Neustart erforderlich",
|
||||
"restart_description": "Reticulum MeshChatX muss neu gestartet werden, damit Schnittstellenänderungen wirksam werden.",
|
||||
"restart_now": "Jetzt neu starten"
|
||||
"restart_now": "Jetzt neu starten",
|
||||
"failed_enable": "failed to enable interface",
|
||||
"failed_disable": "failed to disable interface",
|
||||
"delete_confirm": "Are you sure you want to delete this interface? This can not be undone!",
|
||||
"failed_delete": "failed to delete interface",
|
||||
"failed_export_all": "Failed to export interfaces",
|
||||
"failed_export_single": "Failed to export interface",
|
||||
"failed_reload": "Failed to reload Reticulum!",
|
||||
"interface_not_found": "The selected interface for editing could not be found.",
|
||||
"discovery_settings_saved": "Discovery settings saved",
|
||||
"failed_save_discovery": "Failed to save discovery settings",
|
||||
"no_interfaces_found_config": "No interfaces were found in the selected configuration file",
|
||||
"failed_parse_config": "Failed to parse configuration file",
|
||||
"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"
|
||||
},
|
||||
"map": {
|
||||
"title": "Karte",
|
||||
@@ -390,7 +446,33 @@
|
||||
"no_drawings_desc": "Sie haben noch keine Kartenanmerkungen gespeichert.",
|
||||
"saved_on": "Gespeichert am",
|
||||
"settings": "Karteneinstellungen",
|
||||
"go_to_my_location": "Mein Standort"
|
||||
"go_to_my_location": "Mein Standort",
|
||||
"source_updated": "Map source updated",
|
||||
"failed_set_active": "Failed to set active map",
|
||||
"file_deleted": "File deleted",
|
||||
"failed_delete_file": "Failed to delete file",
|
||||
"storage_saved": "Storage directory saved",
|
||||
"failed_save_storage": "Failed to save directory",
|
||||
"export_cancelled": "Export cancelled",
|
||||
"failed_cancel_export": "Failed to cancel export",
|
||||
"failed_start_export": "Failed to start export",
|
||||
"select_mbtiles_error": "Please select an .mbtiles file",
|
||||
"view_saved": "Default view saved",
|
||||
"failed_save_view": "Failed to save default view",
|
||||
"failed_clear_cache": "Failed to clear cache",
|
||||
"failed_save_tile_server": "Failed to save tile server URL",
|
||||
"failed_save_nominatim": "Failed to save Nominatim API URL",
|
||||
"copied_coordinates": "Copied coordinates",
|
||||
"failed_load_drawings": "Failed to load drawings",
|
||||
"not_initialized": "Map not initialized",
|
||||
"drawing_saved": "Drawing saved",
|
||||
"failed_save_drawing": "Failed to save drawing",
|
||||
"deleted": "Deleted",
|
||||
"failed_delete": "Failed to delete",
|
||||
"location_not_determined": "Could not determine your location",
|
||||
"geolocation_not_supported": "Geolocation is not supported by your browser",
|
||||
"no_nodes_location": "No discovered nodes with location found",
|
||||
"failed_fetch_nodes": "Failed to fetch discovered nodes for mapping"
|
||||
},
|
||||
"interface": {
|
||||
"disable": "Deaktivieren",
|
||||
@@ -454,7 +536,74 @@
|
||||
"generate_paper_message": "Papier-Nachricht erstellen (LXM)",
|
||||
"recording": "Aufnahme: {duration}",
|
||||
"nomad_network_node": "Nomad Network Knoten",
|
||||
"toggle_source": "Quellcode umschalten"
|
||||
"toggle_source": "Quellcode umschalten",
|
||||
"folder_created": "Folder created",
|
||||
"failed_create_folder": "Failed to create folder",
|
||||
"folder_renamed": "Folder renamed",
|
||||
"failed_rename_folder": "Failed to rename folder",
|
||||
"folder_deleted": "Folder deleted",
|
||||
"failed_delete_folder": "Failed to delete folder",
|
||||
"moved_to_folder": "Moved to folder",
|
||||
"failed_move_folder": "Failed to move to folder",
|
||||
"marked_read": "Marked as read",
|
||||
"failed_mark_read": "Failed to mark as read",
|
||||
"conversations_deleted": "Conversations deleted",
|
||||
"failed_delete_conversations": "Failed to delete conversations",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders imported",
|
||||
"failed_import_folders": "Failed to import folders",
|
||||
"failed_read_clipboard": "Failed to read from clipboard",
|
||||
"failed_send_ingest": "Failed to send ingest request",
|
||||
"address_copied": "Your LXMF address copied to clipboard",
|
||||
"failed_copy_address": "Failed to copy address",
|
||||
"translation_failed": "Translation failed",
|
||||
"failed_add_contact": "Failed to add contact",
|
||||
"ingesting_paper_message": "Ingesting paper message...",
|
||||
"failed_ingest_paper": "Failed to ingest paper message",
|
||||
"enter_display_name": "Enter a custom display name",
|
||||
"failed_update_display_name": "Failed to update display name",
|
||||
"failed_load_audio": "Failed to load audio attachment.",
|
||||
"no_contacts_telephone": "No contacts found in telephone",
|
||||
"failed_load_contacts": "Failed to load contacts",
|
||||
"location_request_sent": "Location request sent",
|
||||
"failed_send_location_request": "Failed to send location request",
|
||||
"remove_image_confirm": "Are you sure you want to remove this image attachment?",
|
||||
"failed_start_recording": "failed to start recording",
|
||||
"remove_audio_confirm": "Are you sure you want to remove this audio attachment?",
|
||||
"failed_generate_qr": "Failed to generate QR",
|
||||
"enter_folder_name": "Enter folder name",
|
||||
"new_folder": "New Folder",
|
||||
"rename_folder": "Rename folder",
|
||||
"failed_render_qr": "Failed to render QR code",
|
||||
"uri_copied": "URI copied to clipboard",
|
||||
"failed_copy_uri": "Failed to copy URI",
|
||||
"paper_message_sent": "Paper message sent successfully",
|
||||
"failed_send_paper": "Failed to send paper message",
|
||||
"failed_load_config": "Failed to load configuration",
|
||||
"profile_icon_saved": "Profile icon saved successfully",
|
||||
"failed_save_profile_icon": "Failed to save profile icon",
|
||||
"select_colors_warning": "Please select both background and icon colors",
|
||||
"select_icon_warning": "Please select an icon",
|
||||
"changes_reset": "Changes reset to saved values",
|
||||
"profile_icon_removed": "Profile icon removed successfully",
|
||||
"user_banished": "User banished successfully",
|
||||
"failed_banish_user": "Failed to banish user",
|
||||
"delete_conversation_confirm": "Are you sure you want to delete this conversation?",
|
||||
"failed_delete_conversation": "failed to delete conversation",
|
||||
"delete_history_confirm": "Are you sure you want to delete all messages in this conversation? This can not be undone!",
|
||||
"failed_delete_history": "failed to delete history",
|
||||
"invalid_destination_hash": "Invalid destination hash",
|
||||
"invalid_destination_hash_format": "Invalid destination hash format",
|
||||
"ping_failed": "Ping failed. Try again later",
|
||||
"ping_reply_from": "Valid reply from {hash}",
|
||||
"duration": "Duration: {duration}",
|
||||
"hops_there": "Hops There: {count}",
|
||||
"hops_back": "Hops Back: {count}",
|
||||
"signal_quality": "Signal Quality: {quality}%",
|
||||
"rssi_val": "RSSI: {rssi}dBm",
|
||||
"snr_val": "SNR: {snr}dB",
|
||||
"hash_copied": "Identity hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy identity hash"
|
||||
},
|
||||
"nomadnet": {
|
||||
"remove_favourite": "Favorit entfernen",
|
||||
@@ -494,7 +643,16 @@
|
||||
"enter_nomadnet_url": "Nomadnet-URL eingeben",
|
||||
"archiving_page": "Seite wird archiviert...",
|
||||
"page_archived_successfully": "Seite erfolgreich archiviert.",
|
||||
"identify_confirm": "Sind Sie sicher, dass Sie sich gegenüber diesem NomadNetwork-Knoten identifizieren möchten? Die Seite wird nach dem Senden Ihrer Identität neu geladen."
|
||||
"identify_confirm": "Sind Sie sicher, dass Sie sich gegenüber diesem NomadNetwork-Knoten identifizieren möchten? Die Seite wird nach dem Senden Ihrer Identität neu geladen.",
|
||||
"failed_rename_favourite": "Failed to rename favourite",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment",
|
||||
"enter_section_name": "Enter section name",
|
||||
"new_section": "New Section",
|
||||
"rename_section": "Rename section",
|
||||
"delete_section_confirm": "Delete this section? Favourites will move to the main section.",
|
||||
"unsupported_url": "unsupported url: ",
|
||||
"no_search_results_peers": "No peers found matching your search."
|
||||
},
|
||||
"forwarder": {
|
||||
"title": "LXMF-Weiterleiter",
|
||||
@@ -537,7 +695,10 @@
|
||||
"status_not_downloaded": "Nicht heruntergeladen",
|
||||
"btn_download": "Handbücher herunterladen",
|
||||
"btn_update": "Handbuch aktualisieren",
|
||||
"error": "Fehler"
|
||||
"error": "Fehler",
|
||||
"failed_update_docs": "Failed to update documentation sources",
|
||||
"docs_link_copied": "Documentation link copied to clipboard",
|
||||
"failed_copy_link": "Failed to copy link"
|
||||
},
|
||||
"tools": {
|
||||
"utilities": "Dienstprogramme",
|
||||
@@ -561,7 +722,17 @@
|
||||
},
|
||||
"rnpath": {
|
||||
"title": "RNPath",
|
||||
"description": "Verwalten Sie die Pfadtabelle, sehen Sie Ankündigungsraten und fordern Sie Pfade an."
|
||||
"description": "Verwalten Sie die Pfadtabelle, sehen Sie Ankündigungsraten und fordern Sie Pfade an.",
|
||||
"failed_fetch": "Failed to fetch path data",
|
||||
"path_dropped": "Path dropped",
|
||||
"failed_drop": "Could not drop path",
|
||||
"error_drop": "Error dropping path",
|
||||
"failed_request": "Failed to request path",
|
||||
"paths_dropped": "Paths dropped",
|
||||
"failed_drop_paths": "Failed to drop paths",
|
||||
"purge_confirm": "Purge all announce queues? This cannot be undone.",
|
||||
"queues_purged": "Announce queues purged",
|
||||
"failed_purge": "Failed to purge queues"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Übersetzer",
|
||||
@@ -890,8 +1061,11 @@
|
||||
"available_languages": "Verfügbare Sprachen",
|
||||
"languages_loaded_from": "Sprachen werden von der LibreTranslate-API oder Argos Translate-Paketen geladen.",
|
||||
"refresh_languages": "Sprachen aktualisieren",
|
||||
"failed_to_load_languages": "Sprachen konnten nicht geladen werden. Stellen Sie sicher, dass LibreTranslate ausgeführt wird oder Argos Translate installiert ist.",
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert"
|
||||
"copied_to_clipboard": "In die Zwischenablage kopiert",
|
||||
"failed_load_languages": "Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"failed_translate": "Translation failed. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"select_languages_warning": "Please select both source and target languages.",
|
||||
"auto_detect_not_supported": "Auto-detection is not supported with Argos Translate. Please select a source language."
|
||||
},
|
||||
"call": {
|
||||
"phone": "Telefon",
|
||||
@@ -1012,7 +1186,36 @@
|
||||
"failed_to_set_primary_ringtone": "Primärer Klingelton konnte nicht festgelegt werden",
|
||||
"failed_to_update_ringtone_name": "Klingeltonname konnte nicht aktualisiert werden",
|
||||
"failed_to_play_ringtone": "Klingelton konnte nicht abgespielt werden",
|
||||
"failed_to_play_voicemail": "Sprachnachricht konnte nicht abgespielt werden"
|
||||
"failed_to_play_voicemail": "Sprachnachricht konnte nicht abgespielt werden",
|
||||
"web_audio_not_available": "Web audio not available",
|
||||
"failed_to_update_dnd": "Failed to update Do Not Disturb status",
|
||||
"failed_to_update_call_settings": "Failed to update call settings",
|
||||
"failed_to_update_recording_status": "Failed to update call recording status",
|
||||
"call_history_cleared": "Call history cleared",
|
||||
"failed_to_clear_call_history": "Failed to clear call history",
|
||||
"identity_banished": "Identity banished",
|
||||
"failed_to_banish_identity": "Failed to banish identity",
|
||||
"name_and_hash_required": "Name and identity hash required",
|
||||
"contact_updated": "Contact updated",
|
||||
"contact_added": "Contact added",
|
||||
"contact_deleted": "Contact deleted",
|
||||
"failed_to_delete_contact": "Failed to delete contact",
|
||||
"hash_copied": "Hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy hash",
|
||||
"greeting_uploaded_successfully": "Greeting uploaded successfully",
|
||||
"greeting_deleted": "Greeting deleted",
|
||||
"failed_to_delete_greeting": "Failed to delete greeting",
|
||||
"failed_to_start_recording_greeting": "Failed to start recording greeting",
|
||||
"greeting_recorded_from_mic": "Greeting recorded from mic",
|
||||
"failed_to_stop_recording_greeting": "Failed to stop recording greeting",
|
||||
"failed_to_load_recording": "Failed to load recording audio",
|
||||
"recording_deleted": "Recording deleted",
|
||||
"failed_to_delete_recording": "Failed to delete recording",
|
||||
"call_sent_to_voicemail": "Call sent to voicemail",
|
||||
"failed_to_send_to_voicemail": "Failed to send call to voicemail",
|
||||
"failed_load_audio_edit": "Failed to load audio for editing",
|
||||
"ringtone_saved": "Ringtone saved successfully",
|
||||
"failed_save_ringtone": "Failed to save edited ringtone"
|
||||
},
|
||||
"tutorial": {
|
||||
"title": "Erste Schritte",
|
||||
@@ -1091,7 +1294,9 @@
|
||||
"discovery_question": "Möchten Sie die Community-Schnittstellenerkennung und Auto-Connect verwenden?",
|
||||
"discovery_desc": "Dies ermöglicht es MeshChatX, automatisch öffentliche Community-Knoten in Ihrer Nähe oder im Internet zu finden und sich mit ihnen zu verbinden.",
|
||||
"yes": "Ja, Erkennung verwenden",
|
||||
"no": "Nein, manuelle Einrichtung"
|
||||
"no": "Nein, manuelle Einrichtung",
|
||||
"discovery_enabled": "Community discovery enabled",
|
||||
"failed_enable_discovery": "Failed to enable discovery"
|
||||
},
|
||||
"command_palette": {
|
||||
"search_placeholder": "Befehle suchen, navigieren oder Peers finden...",
|
||||
@@ -1144,6 +1349,97 @@
|
||||
"action_getting_started": "Erste Schritte",
|
||||
"action_getting_started_desc": "Einführungsleitfaden anzeigen",
|
||||
"action_changelog": "Änderungsprotokoll",
|
||||
"action_changelog_desc": "Was ist neu anzeigen"
|
||||
"action_changelog_desc": "Was ist neu anzeigen",
|
||||
"action_bouncing_balls": "Bouncing Balls",
|
||||
"action_bouncing_balls_desc": "Toggle bouncing balls mode"
|
||||
},
|
||||
"settings": {
|
||||
"shortcut_saved": "Shortcut saved",
|
||||
"shortcut_deleted": "Shortcut deleted",
|
||||
"archived_pages_flushed": "Archived pages flushed.",
|
||||
"failed_enable_transport": "Failed to enable transport mode!",
|
||||
"failed_disable_transport": "Failed to disable transport mode!",
|
||||
"failed_reload_reticulum": "Failed to reload Reticulum!",
|
||||
"folders_exported": "Folders exported",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders and mappings imported successfully",
|
||||
"failed_import_folders": "Failed to import folders"
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug Logs",
|
||||
"description": "View application logs and detected anomalies.",
|
||||
"failed_fetch_logs": "Failed to fetch logs",
|
||||
"logs_copied": "Logs on this page copied to clipboard",
|
||||
"failed_copy_logs": "Failed to copy logs"
|
||||
},
|
||||
"visualiser": {
|
||||
"title": "Network Visualiser",
|
||||
"description": "Visualize the mesh network topology.",
|
||||
"reticulum_mesh": "Reticulum Mesh",
|
||||
"network_visualizer": "Network Visualizer",
|
||||
"node_info": "Node Info",
|
||||
"hash": "Hash",
|
||||
"hops": "Hops",
|
||||
"interface": "Interface",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"first_seen": "First seen",
|
||||
"last_seen": "Last seen",
|
||||
"center_node": "Center on node",
|
||||
"copy_hash": "Copy Hash",
|
||||
"banish_identity": "Banish identity",
|
||||
"banished_identities": "Banished identities",
|
||||
"filter_nodes": "Filter nodes...",
|
||||
"all": "All",
|
||||
"direct": "Direct",
|
||||
"one_hop": "1 Hop",
|
||||
"multi_hops": "2+ Hops",
|
||||
"clear": "Clear",
|
||||
"show_labels": "Show all labels",
|
||||
"bouncing_balls": "Bouncing balls mode",
|
||||
"orbit_mode": "Orbit mode",
|
||||
"show_edges": "Show edges",
|
||||
"show_tooltips": "Show tooltips",
|
||||
"show_rssi": "Show RSSI",
|
||||
"show_snr": "Show SNR",
|
||||
"show_hops": "Show Hops",
|
||||
"physics": "Physics",
|
||||
"gravity": "Gravity",
|
||||
"spring_length": "Spring length",
|
||||
"spring_strength": "Spring strength",
|
||||
"damping": "Damping",
|
||||
"central_gravity": "Central gravity",
|
||||
"edge_strength": "Edge strength",
|
||||
"force_atlas_2": "Force Atlas 2",
|
||||
"barns_hut": "Barns Hut",
|
||||
"repulsion": "Repulsion",
|
||||
"reset_physics": "Reset physics",
|
||||
"reset_camera": "Reset Camera",
|
||||
"clear_graph": "Clear graph",
|
||||
"total_nodes": "Total nodes",
|
||||
"total_edges": "Total edges",
|
||||
"discovered": "Discovered",
|
||||
"wait_between_batches": "Wait between batches",
|
||||
"batch": "Batch",
|
||||
"loading_status": "Loading status",
|
||||
"failed_load": "Failed to load network data"
|
||||
},
|
||||
"banishment": {
|
||||
"title": "Banished",
|
||||
"description": "Manage Banished users and nodes",
|
||||
"failed_load_banished": "Failed to load banished destinations",
|
||||
"search_placeholder": "Search by hash or display name...",
|
||||
"loading_items": "Loading banished items...",
|
||||
"no_items": "No banished items found",
|
||||
"lift_banishment": "Lift Banishment",
|
||||
"lift_banishment_confirm": "Are you sure you want to lift the banishment for {name}?",
|
||||
"banished_at": "Banished at",
|
||||
"banished_until": "Banished until",
|
||||
"reason": "Reason",
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"node": "Node",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment"
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,8 @@
|
||||
"lxmf_address": "LXMF Address",
|
||||
"announce": "Announce",
|
||||
"announce_now": "Announce Now",
|
||||
"show_qr": "Show QR Code",
|
||||
"failed_announce": "failed to announce",
|
||||
"last_announced": "Last announced: {time}",
|
||||
"last_announced_never": "Last announced: Never",
|
||||
"display_name_placeholder": "Display Name",
|
||||
@@ -123,7 +125,7 @@
|
||||
"not_synced": "Not Synced",
|
||||
"not_configured": "Not Configured",
|
||||
"toggle_source": "Toggle Source Code",
|
||||
"audio_calls": "Telephone",
|
||||
"audio_calls": "Audio Calls",
|
||||
"calls": "Calls",
|
||||
"status": "Status",
|
||||
"active_call": "Active Call",
|
||||
@@ -145,6 +147,7 @@
|
||||
"propagation_node": "Propagation Node",
|
||||
"sync_now": "Sync Now",
|
||||
"setting_auto_saved": "Setting {label} auto-saved",
|
||||
"search_settings": "Search settings...",
|
||||
"auto_resend": "Auto Resend",
|
||||
"retry_attachments": "Retry Attachments",
|
||||
"auto_fallback": "Auto Fallback",
|
||||
@@ -205,15 +208,27 @@
|
||||
"restart_app": "Restart App",
|
||||
"reveal": "Reveal",
|
||||
"refresh": "Refresh",
|
||||
"copy": "Copy",
|
||||
"copy_to_clipboard": "Copy to clipboard",
|
||||
"vacuum": "Vacuum",
|
||||
"auto_recover": "Auto Recover",
|
||||
"shutdown": "Shutdown",
|
||||
"acknowledge_reset": "Acknowledge & Reset",
|
||||
"continue": "Continue",
|
||||
"confirm": "Confirm",
|
||||
"delete_confirm": "Are you sure you want to delete this? This cannot be undone.",
|
||||
"search": "Search tools...",
|
||||
"no_results": "No tools found",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"copied": "Copied to clipboard",
|
||||
"failed_to_copy": "Failed to copy to clipboard",
|
||||
"save_failed": "Failed to save configuration!",
|
||||
"invalid_address": "Invalid Address",
|
||||
"loading": "Loading...",
|
||||
"ok": "OK"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Maintenance & Data",
|
||||
@@ -254,7 +269,13 @@
|
||||
"delete_confirm": "Are you sure you want to delete identity \"{name}\"? This cannot be undone.",
|
||||
"switched": "Identity switched. The application will now restart to apply changes.",
|
||||
"created": "Identity created successfully",
|
||||
"deleted": "Identity deleted"
|
||||
"deleted": "Identity deleted",
|
||||
"failed_load": "Failed to load identities",
|
||||
"enter_display_name_warning": "Please enter a display name",
|
||||
"failed_create": "Failed to create identity",
|
||||
"switch_scheduled": "Switch scheduled. Reloading application...",
|
||||
"failed_switch": "Failed to switch identity",
|
||||
"failed_delete": "Failed to delete identity"
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
@@ -302,10 +323,29 @@
|
||||
"secured": "Secured",
|
||||
"tampering_detected": "Tampering Detected",
|
||||
"technical_issues": "Technical Issues:",
|
||||
"integrity_backend_error": "The application backend binary (unpacked from ASAR) appears to have been modified or replaced. This could indicate a malicious actor trying to compromise your mesh communication.",
|
||||
"integrity_data_error": "Your identities or database files appear to have been modified while the app was closed.",
|
||||
"integrity_warning_footer": "Proceed with caution. If you did not manually update or modify these files, your installation may be compromised.",
|
||||
"no_integrity_violations": "No integrity violations detected since last startup.",
|
||||
"dependency_chain": "Dependency Chain",
|
||||
"other_core_components": "Other Core Components",
|
||||
"backend_dependencies": "Backend Dependencies"
|
||||
"backend_dependencies": "Backend Dependencies",
|
||||
"delete_snapshot_confirm": "Are you sure you want to delete this snapshot?",
|
||||
"snapshot_deleted": "Snapshot deleted",
|
||||
"failed_delete_snapshot": "Failed to delete snapshot",
|
||||
"delete_backup_confirm": "Are you sure you want to delete this backup?",
|
||||
"backup_deleted": "Backup deleted",
|
||||
"failed_delete_backup": "Failed to delete backup",
|
||||
"database_restored": "Database restored. Relaunching...",
|
||||
"failed_restore_snapshot": "Failed to restore snapshot",
|
||||
"restore_snapshot_confirm": "Are you sure you want to restore this snapshot? This will overwrite the current database and require an app relaunch.",
|
||||
"integrity_acknowledged": "Integrity issues acknowledged",
|
||||
"integrity_acknowledged_reset": "Integrity issues acknowledged and manifest reset",
|
||||
"integrity_acknowledge_confirm": "Are you sure you want to acknowledge these integrity issues? This will update the security manifest to match the current state of your files.",
|
||||
"failed_acknowledge_integrity": "Failed to acknowledge integrity issues",
|
||||
"shutdown_sent": "Shutdown command sent to server.",
|
||||
"identity_exported": "Identity key file exported",
|
||||
"identity_copied": "Identity Base32 key copied to clipboard"
|
||||
},
|
||||
"interfaces": {
|
||||
"title": "Interfaces",
|
||||
@@ -321,7 +361,23 @@
|
||||
"no_interfaces_description": "Adjust your search or add a new interface.",
|
||||
"restart_required": "Restart required",
|
||||
"restart_description": "Reticulum MeshChatX must be restarted for any interface changes to take effect.",
|
||||
"restart_now": "Restart now"
|
||||
"restart_now": "Restart now",
|
||||
"failed_enable": "failed to enable interface",
|
||||
"failed_disable": "failed to disable interface",
|
||||
"delete_confirm": "Are you sure you want to delete this interface? This can not be undone!",
|
||||
"failed_delete": "failed to delete interface",
|
||||
"failed_export_all": "Failed to export interfaces",
|
||||
"failed_export_single": "Failed to export interface",
|
||||
"failed_reload": "Failed to reload Reticulum!",
|
||||
"interface_not_found": "The selected interface for editing could not be found.",
|
||||
"discovery_settings_saved": "Discovery settings saved",
|
||||
"failed_save_discovery": "Failed to save discovery settings",
|
||||
"no_interfaces_found_config": "No interfaces were found in the selected configuration file",
|
||||
"failed_parse_config": "Failed to parse configuration file",
|
||||
"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"
|
||||
},
|
||||
"map": {
|
||||
"title": "Map",
|
||||
@@ -390,7 +446,33 @@
|
||||
"no_drawings_desc": "You haven't saved any map annotations yet.",
|
||||
"saved_on": "Saved on",
|
||||
"settings": "Map Settings",
|
||||
"go_to_my_location": "Go to My Location"
|
||||
"go_to_my_location": "Go to My Location",
|
||||
"source_updated": "Map source updated",
|
||||
"failed_set_active": "Failed to set active map",
|
||||
"file_deleted": "File deleted",
|
||||
"failed_delete_file": "Failed to delete file",
|
||||
"storage_saved": "Storage directory saved",
|
||||
"failed_save_storage": "Failed to save directory",
|
||||
"export_cancelled": "Export cancelled",
|
||||
"failed_cancel_export": "Failed to cancel export",
|
||||
"failed_start_export": "Failed to start export",
|
||||
"select_mbtiles_error": "Please select an .mbtiles file",
|
||||
"view_saved": "Default view saved",
|
||||
"failed_save_view": "Failed to save default view",
|
||||
"failed_clear_cache": "Failed to clear cache",
|
||||
"failed_save_tile_server": "Failed to save tile server URL",
|
||||
"failed_save_nominatim": "Failed to save Nominatim API URL",
|
||||
"copied_coordinates": "Copied coordinates",
|
||||
"failed_load_drawings": "Failed to load drawings",
|
||||
"not_initialized": "Map not initialized",
|
||||
"drawing_saved": "Drawing saved",
|
||||
"failed_save_drawing": "Failed to save drawing",
|
||||
"deleted": "Deleted",
|
||||
"failed_delete": "Failed to delete",
|
||||
"location_not_determined": "Could not determine your location",
|
||||
"geolocation_not_supported": "Geolocation is not supported by your browser",
|
||||
"no_nodes_location": "No discovered nodes with location found",
|
||||
"failed_fetch_nodes": "Failed to fetch discovered nodes for mapping"
|
||||
},
|
||||
"interface": {
|
||||
"disable": "Disable",
|
||||
@@ -454,7 +536,163 @@
|
||||
"generate_paper_message": "Generate Paper Message (LXM)",
|
||||
"recording": "Recording: {duration}",
|
||||
"nomad_network_node": "Nomad Network Node",
|
||||
"toggle_source": "Toggle Source Code"
|
||||
"toggle_source": "Toggle Source Code",
|
||||
"folder_created": "Folder created",
|
||||
"failed_create_folder": "Failed to create folder",
|
||||
"folder_renamed": "Folder renamed",
|
||||
"failed_rename_folder": "Failed to rename folder",
|
||||
"folder_deleted": "Folder deleted",
|
||||
"failed_delete_folder": "Failed to delete folder",
|
||||
"moved_to_folder": "Moved to folder",
|
||||
"failed_move_folder": "Failed to move to folder",
|
||||
"marked_read": "Marked as read",
|
||||
"failed_mark_read": "Failed to mark as read",
|
||||
"conversations_deleted": "Conversations deleted",
|
||||
"failed_delete_conversations": "Failed to delete conversations",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders imported",
|
||||
"failed_import_folders": "Failed to import folders",
|
||||
"failed_read_clipboard": "Failed to read from clipboard",
|
||||
"failed_send_ingest": "Failed to send ingest request",
|
||||
"address_copied": "Your LXMF address copied to clipboard",
|
||||
"hash_copied": "Identity hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy identity hash",
|
||||
"failed_copy_address": "Failed to copy address",
|
||||
"translation_failed": "Translation failed",
|
||||
"failed_add_contact": "Failed to add contact",
|
||||
"ingesting_paper_message": "Ingesting paper message...",
|
||||
"failed_ingest_paper": "Failed to ingest paper message",
|
||||
"enter_display_name": "Enter a custom display name",
|
||||
"failed_update_display_name": "Failed to update display name",
|
||||
"failed_load_audio": "Failed to load audio attachment.",
|
||||
"no_contacts_telephone": "No contacts found in telephone",
|
||||
"failed_load_contacts": "Failed to load contacts",
|
||||
"location_request_sent": "Location request sent",
|
||||
"failed_send_location_request": "Failed to send location request",
|
||||
"remove_image_confirm": "Are you sure you want to remove this image attachment?",
|
||||
"failed_start_recording": "failed to start recording",
|
||||
"remove_audio_confirm": "Are you sure you want to remove this audio attachment?",
|
||||
"failed_generate_qr": "Failed to generate QR",
|
||||
"enter_folder_name": "Enter folder name",
|
||||
"new_folder": "New Folder",
|
||||
"rename_folder": "Rename folder",
|
||||
"failed_render_qr": "Failed to render QR code",
|
||||
"uri_copied": "URI copied to clipboard",
|
||||
"failed_copy_uri": "Failed to copy URI",
|
||||
"paper_message_sent": "Paper message sent successfully",
|
||||
"failed_send_paper": "Failed to send paper message",
|
||||
"failed_load_config": "Failed to load configuration",
|
||||
"profile_icon_saved": "Profile icon saved successfully",
|
||||
"failed_save_profile_icon": "Failed to save profile icon",
|
||||
"select_colors_warning": "Please select both background and icon colors",
|
||||
"select_icon_warning": "Please select an icon",
|
||||
"changes_reset": "Changes reset to saved values",
|
||||
"profile_icon_removed": "Profile icon removed successfully",
|
||||
"user_banished": "User banished successfully",
|
||||
"failed_banish_user": "Failed to banish user",
|
||||
"delete_conversation_confirm": "Are you sure you want to delete this conversation?",
|
||||
"failed_delete_conversation": "failed to delete conversation",
|
||||
"delete_history_confirm": "Are you sure you want to delete all messages in this conversation? This can not be undone!",
|
||||
"failed_delete_history": "failed to delete history",
|
||||
"invalid_destination_hash": "Invalid destination hash",
|
||||
"invalid_destination_hash_format": "Invalid destination hash format",
|
||||
"ping_failed": "Ping failed. Try again later",
|
||||
"ping_reply_from": "Valid reply from {hash}",
|
||||
"duration": "Duration: {duration}",
|
||||
"hops_there": "Hops There: {count}",
|
||||
"hops_back": "Hops Back: {count}",
|
||||
"signal_quality": "Signal Quality: {quality}%",
|
||||
"rssi_val": "RSSI: {rssi}dBm",
|
||||
"snr_val": "SNR: {snr}dB"
|
||||
},
|
||||
"settings": {
|
||||
"shortcut_saved": "Shortcut saved",
|
||||
"shortcut_deleted": "Shortcut deleted",
|
||||
"archived_pages_flushed": "Archived pages flushed.",
|
||||
"failed_enable_transport": "Failed to enable transport mode!",
|
||||
"failed_disable_transport": "Failed to disable transport mode!",
|
||||
"failed_reload_reticulum": "Failed to reload Reticulum!",
|
||||
"folders_exported": "Folders exported",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders and mappings imported successfully",
|
||||
"failed_import_folders": "Failed to import folders"
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug Logs",
|
||||
"description": "View application logs and detected anomalies.",
|
||||
"failed_fetch_logs": "Failed to fetch logs",
|
||||
"logs_copied": "Logs on this page copied to clipboard",
|
||||
"failed_copy_logs": "Failed to copy logs"
|
||||
},
|
||||
"visualiser": {
|
||||
"title": "Network Visualiser",
|
||||
"description": "Visualize the mesh network topology.",
|
||||
"reticulum_mesh": "Reticulum Mesh",
|
||||
"network_visualizer": "Network Visualizer",
|
||||
"node_info": "Node Info",
|
||||
"hash": "Hash",
|
||||
"hops": "Hops",
|
||||
"interface": "Interface",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"first_seen": "First seen",
|
||||
"last_seen": "Last seen",
|
||||
"center_node": "Center on node",
|
||||
"copy_hash": "Copy Hash",
|
||||
"banish_identity": "Banish identity",
|
||||
"banished_identities": "Banished identities",
|
||||
"filter_nodes": "Filter nodes...",
|
||||
"all": "All",
|
||||
"direct": "Direct",
|
||||
"one_hop": "1 Hop",
|
||||
"multi_hops": "2+ Hops",
|
||||
"clear": "Clear",
|
||||
"show_labels": "Show all labels",
|
||||
"bouncing_balls": "Bouncing balls mode",
|
||||
"orbit_mode": "Orbit mode",
|
||||
"show_edges": "Show edges",
|
||||
"show_tooltips": "Show tooltips",
|
||||
"show_rssi": "Show RSSI",
|
||||
"show_snr": "Show SNR",
|
||||
"show_hops": "Show Hops",
|
||||
"physics": "Physics",
|
||||
"gravity": "Gravity",
|
||||
"spring_length": "Spring length",
|
||||
"spring_strength": "Spring strength",
|
||||
"damping": "Damping",
|
||||
"central_gravity": "Central gravity",
|
||||
"edge_strength": "Edge strength",
|
||||
"force_atlas_2": "Force Atlas 2",
|
||||
"barns_hut": "Barns Hut",
|
||||
"repulsion": "Repulsion",
|
||||
"reset_physics": "Reset physics",
|
||||
"reset_camera": "Reset Camera",
|
||||
"clear_graph": "Clear graph",
|
||||
"total_nodes": "Total nodes",
|
||||
"total_edges": "Total edges",
|
||||
"discovered": "Discovered",
|
||||
"wait_between_batches": "Wait between batches",
|
||||
"batch": "Batch",
|
||||
"loading_status": "Loading status",
|
||||
"failed_load": "Failed to load network data"
|
||||
},
|
||||
"banishment": {
|
||||
"title": "Banished",
|
||||
"description": "Manage Banished users and nodes",
|
||||
"failed_load_banished": "Failed to load banished destinations",
|
||||
"search_placeholder": "Search by hash or display name...",
|
||||
"loading_items": "Loading banished items...",
|
||||
"no_items": "No banished items found",
|
||||
"lift_banishment": "Lift Banishment",
|
||||
"lift_banishment_confirm": "Are you sure you want to lift the banishment for {name}?",
|
||||
"banished_at": "Banished at",
|
||||
"banished_until": "Banished until",
|
||||
"reason": "Reason",
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"node": "Node",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment"
|
||||
},
|
||||
"nomadnet": {
|
||||
"remove_favourite": "Remove Favourite",
|
||||
@@ -485,16 +723,25 @@
|
||||
"announced_time_ago": "Announced {time} ago",
|
||||
"block_node": "Banish Node",
|
||||
"no_announces_yet": "No announces yet",
|
||||
"no_search_results_peers": "No peers found matching your search.",
|
||||
"listening_for_peers": "Listening for peers on the mesh.",
|
||||
"block_node_confirm": "Are you sure you want to banish {name}? Their announces will be ignored and they won't appear in the announce stream.",
|
||||
"node_blocked_successfully": "Node banished successfully",
|
||||
"failed_to_block_node": "Failed to banish node",
|
||||
"failed_rename_favourite": "Failed to rename favourite",
|
||||
"rename_favourite": "Rename this favourite",
|
||||
"remove_favourite_confirm": "Are you sure you want to remove this favourite?",
|
||||
"enter_nomadnet_url": "Enter a Nomadnet URL",
|
||||
"archiving_page": "Archiving page...",
|
||||
"page_archived_successfully": "Page archived successfully.",
|
||||
"identify_confirm": "Are you sure you want to identify yourself to this NomadNetwork Node? The page will reload after your identity has been sent."
|
||||
"identify_confirm": "Are you sure you want to identify yourself to this NomadNetwork Node? The page will reload after your identity has been sent.",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment",
|
||||
"enter_section_name": "Enter section name",
|
||||
"new_section": "New Section",
|
||||
"rename_section": "Rename section",
|
||||
"delete_section_confirm": "Delete this section? Favourites will move to the main section.",
|
||||
"unsupported_url": "unsupported url: "
|
||||
},
|
||||
"forwarder": {
|
||||
"title": "LXMF Forwarder",
|
||||
@@ -537,7 +784,10 @@
|
||||
"status_not_downloaded": "Not Downloaded",
|
||||
"btn_download": "Download Manuals",
|
||||
"btn_update": "Update Manual",
|
||||
"error": "Error"
|
||||
"error": "Error",
|
||||
"failed_update_docs": "Failed to update documentation sources",
|
||||
"docs_link_copied": "Documentation link copied to clipboard",
|
||||
"failed_copy_link": "Failed to copy link"
|
||||
},
|
||||
"tools": {
|
||||
"utilities": "Utilities",
|
||||
@@ -561,7 +811,17 @@
|
||||
},
|
||||
"rnpath": {
|
||||
"title": "RNPath",
|
||||
"description": "Manage the path table, view announce rates and request paths."
|
||||
"description": "Manage the path table, view announce rates and request paths.",
|
||||
"failed_fetch": "Failed to fetch path data",
|
||||
"path_dropped": "Path dropped",
|
||||
"failed_drop": "Could not drop path",
|
||||
"error_drop": "Error dropping path",
|
||||
"failed_request": "Failed to request path",
|
||||
"paths_dropped": "Paths dropped",
|
||||
"failed_drop_paths": "Failed to drop paths",
|
||||
"purge_confirm": "Purge all announce queues? This cannot be undone.",
|
||||
"queues_purged": "Announce queues purged",
|
||||
"failed_purge": "Failed to purge queues"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
@@ -890,7 +1150,10 @@
|
||||
"available_languages": "Available Languages",
|
||||
"languages_loaded_from": "Languages are loaded from LibreTranslate API or Argos Translate packages.",
|
||||
"refresh_languages": "Refresh Languages",
|
||||
"failed_to_load_languages": "Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"failed_load_languages": "Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"failed_translate": "Translation failed. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"select_languages_warning": "Please select both source and target languages.",
|
||||
"auto_detect_not_supported": "Auto-detection is not supported with Argos Translate. Please select a source language.",
|
||||
"copied_to_clipboard": "Copied to clipboard"
|
||||
},
|
||||
"call": {
|
||||
@@ -1012,7 +1275,36 @@
|
||||
"failed_to_set_primary_ringtone": "Failed to set primary ringtone",
|
||||
"failed_to_update_ringtone_name": "Failed to update ringtone name",
|
||||
"failed_to_play_ringtone": "Failed to play ringtone",
|
||||
"failed_to_play_voicemail": "Failed to play voicemail"
|
||||
"failed_to_play_voicemail": "Failed to play voicemail",
|
||||
"web_audio_not_available": "Web audio not available",
|
||||
"failed_to_update_dnd": "Failed to update Do Not Disturb status",
|
||||
"failed_to_update_call_settings": "Failed to update call settings",
|
||||
"failed_to_update_recording_status": "Failed to update call recording status",
|
||||
"call_history_cleared": "Call history cleared",
|
||||
"failed_to_clear_call_history": "Failed to clear call history",
|
||||
"identity_banished": "Identity banished",
|
||||
"failed_to_banish_identity": "Failed to banish identity",
|
||||
"name_and_hash_required": "Name and identity hash required",
|
||||
"contact_updated": "Contact updated",
|
||||
"contact_added": "Contact added",
|
||||
"contact_deleted": "Contact deleted",
|
||||
"failed_to_delete_contact": "Failed to delete contact",
|
||||
"hash_copied": "Hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy hash",
|
||||
"greeting_uploaded_successfully": "Greeting uploaded successfully",
|
||||
"greeting_deleted": "Greeting deleted",
|
||||
"failed_to_delete_greeting": "Failed to delete greeting",
|
||||
"failed_to_start_recording_greeting": "Failed to start recording greeting",
|
||||
"greeting_recorded_from_mic": "Greeting recorded from mic",
|
||||
"failed_to_stop_recording_greeting": "Failed to stop recording greeting",
|
||||
"failed_to_load_recording": "Failed to load recording audio",
|
||||
"recording_deleted": "Recording deleted",
|
||||
"failed_to_delete_recording": "Failed to delete recording",
|
||||
"call_sent_to_voicemail": "Call sent to voicemail",
|
||||
"failed_to_send_to_voicemail": "Failed to send call to voicemail",
|
||||
"failed_load_audio_edit": "Failed to load audio for editing",
|
||||
"ringtone_saved": "Ringtone saved successfully",
|
||||
"failed_save_ringtone": "Failed to save edited ringtone"
|
||||
},
|
||||
"tutorial": {
|
||||
"title": "Getting Started",
|
||||
@@ -1091,7 +1383,9 @@
|
||||
"discovery_question": "Do you want to use community interface discovering and auto-connect?",
|
||||
"discovery_desc": "This allows MeshChatX to automatically find and connect to public community nodes near you or on the internet.",
|
||||
"yes": "Yes, use discovery",
|
||||
"no": "No, manual setup"
|
||||
"no": "No, manual setup",
|
||||
"discovery_enabled": "Community discovery enabled",
|
||||
"failed_enable_discovery": "Failed to enable discovery"
|
||||
},
|
||||
"command_palette": {
|
||||
"search_placeholder": "Search commands, navigate, or find peers...",
|
||||
@@ -1141,6 +1435,8 @@
|
||||
"action_compose_desc": "Start a new message",
|
||||
"action_orbit": "Toggle Orbit",
|
||||
"action_orbit_desc": "Toggle orbit mode",
|
||||
"action_bouncing_balls": "Bouncing Balls",
|
||||
"action_bouncing_balls_desc": "Toggle bouncing balls mode",
|
||||
"action_getting_started": "Getting Started",
|
||||
"action_getting_started_desc": "Show getting started guide",
|
||||
"action_changelog": "Changelog",
|
||||
|
||||
@@ -190,7 +190,10 @@
|
||||
"loading_identity": "Загрузка вашей личности",
|
||||
"emergency_mode_active": "Аварийный режим активен — используется база данных в оперативной памяти и ограниченные службы.",
|
||||
"blackhole_integration_enabled": "Интеграция Blackhole",
|
||||
"blackhole_integration_description": "Автоматически блокировать (blackhole) личности на транспортном уровне Reticulum при изгнании пользователей в MeshChatX."
|
||||
"blackhole_integration_description": "Автоматически блокировать (blackhole) личности на транспортном уровне Reticulum при изгнании пользователей в MeshChatX.",
|
||||
"failed_announce": "failed to announce",
|
||||
"search_settings": "Search settings...",
|
||||
"show_qr": "Show QR Code"
|
||||
},
|
||||
"common": {
|
||||
"open": "Открыть",
|
||||
@@ -213,7 +216,19 @@
|
||||
"delete_confirm": "Вы уверены, что хотите удалить это? Это действие нельзя отменить.",
|
||||
"search": "Поиск инструментов...",
|
||||
"no_results": "Инструменты не найдены",
|
||||
"error": "Ошибка"
|
||||
"error": "Ошибка",
|
||||
"continue": "Continue",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"copied": "Copied to clipboard",
|
||||
"failed_to_copy": "Failed to copy to clipboard",
|
||||
"save_failed": "Failed to save configuration!",
|
||||
"invalid_address": "Invalid Address",
|
||||
"loading": "Loading...",
|
||||
"ok": "OK",
|
||||
"copy": "Copy",
|
||||
"copy_to_clipboard": "Copy to clipboard"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Обслуживание и данные",
|
||||
@@ -254,7 +269,13 @@
|
||||
"delete_confirm": "Вы уверены, что хотите удалить личность \"{name}\"? Это действие нельзя отменить.",
|
||||
"switched": "Личность изменена. Приложение будет перезапущено для применения изменений.",
|
||||
"created": "Личность успешно создана",
|
||||
"deleted": "Личность удалена"
|
||||
"deleted": "Личность удалена",
|
||||
"failed_load": "Failed to load identities",
|
||||
"enter_display_name_warning": "Please enter a display name",
|
||||
"failed_create": "Failed to create identity",
|
||||
"switch_scheduled": "Switch scheduled. Reloading application...",
|
||||
"failed_switch": "Failed to switch identity",
|
||||
"failed_delete": "Failed to delete identity"
|
||||
},
|
||||
"about": {
|
||||
"title": "О программе",
|
||||
@@ -305,7 +326,26 @@
|
||||
"no_integrity_violations": "Нарушений целостности не обнаружено с момента последнего запуска.",
|
||||
"dependency_chain": "Цепочка зависимостей",
|
||||
"other_core_components": "Другие основные компоненты",
|
||||
"backend_dependencies": "Зависимости бэкенда"
|
||||
"backend_dependencies": "Зависимости бэкенда",
|
||||
"integrity_backend_error": "The application backend binary (unpacked from ASAR) appears to have been modified or replaced. This could indicate a malicious actor trying to compromise your mesh communication.",
|
||||
"integrity_data_error": "Your identities or database files appear to have been modified while the app was closed.",
|
||||
"integrity_warning_footer": "Proceed with caution. If you did not manually update or modify these files, your installation may be compromised.",
|
||||
"delete_snapshot_confirm": "Are you sure you want to delete this snapshot?",
|
||||
"snapshot_deleted": "Snapshot deleted",
|
||||
"failed_delete_snapshot": "Failed to delete snapshot",
|
||||
"delete_backup_confirm": "Are you sure you want to delete this backup?",
|
||||
"backup_deleted": "Backup deleted",
|
||||
"failed_delete_backup": "Failed to delete backup",
|
||||
"database_restored": "Database restored. Relaunching...",
|
||||
"failed_restore_snapshot": "Failed to restore snapshot",
|
||||
"restore_snapshot_confirm": "Are you sure you want to restore this snapshot? This will overwrite the current database and require an app relaunch.",
|
||||
"integrity_acknowledged": "Integrity issues acknowledged",
|
||||
"integrity_acknowledged_reset": "Integrity issues acknowledged and manifest reset",
|
||||
"integrity_acknowledge_confirm": "Are you sure you want to acknowledge these integrity issues? This will update the security manifest to match the current state of your files.",
|
||||
"failed_acknowledge_integrity": "Failed to acknowledge integrity issues",
|
||||
"shutdown_sent": "Shutdown command sent to server.",
|
||||
"identity_exported": "Identity key file exported",
|
||||
"identity_copied": "Identity Base32 key copied to clipboard"
|
||||
},
|
||||
"interfaces": {
|
||||
"title": "Интерфейсы",
|
||||
@@ -321,7 +361,23 @@
|
||||
"no_interfaces_description": "Измените параметры поиска или добавьте новый интерфейс.",
|
||||
"restart_required": "Требуется перезапуск",
|
||||
"restart_description": "Reticulum MeshChatX необходимо перезапустить, чтобы изменения вступили в силу.",
|
||||
"restart_now": "Перезапустить сейчас"
|
||||
"restart_now": "Перезапустить сейчас",
|
||||
"failed_enable": "failed to enable interface",
|
||||
"failed_disable": "failed to disable interface",
|
||||
"delete_confirm": "Are you sure you want to delete this interface? This can not be undone!",
|
||||
"failed_delete": "failed to delete interface",
|
||||
"failed_export_all": "Failed to export interfaces",
|
||||
"failed_export_single": "Failed to export interface",
|
||||
"failed_reload": "Failed to reload Reticulum!",
|
||||
"interface_not_found": "The selected interface for editing could not be found.",
|
||||
"discovery_settings_saved": "Discovery settings saved",
|
||||
"failed_save_discovery": "Failed to save discovery settings",
|
||||
"no_interfaces_found_config": "No interfaces were found in the selected configuration file",
|
||||
"failed_parse_config": "Failed to parse configuration file",
|
||||
"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"
|
||||
},
|
||||
"map": {
|
||||
"title": "Карта",
|
||||
@@ -390,7 +446,33 @@
|
||||
"no_drawings_desc": "Вы еще не сохранили ни одной аннотации на карте.",
|
||||
"saved_on": "Сохранено",
|
||||
"settings": "Настройки карты",
|
||||
"go_to_my_location": "Мое местоположение"
|
||||
"go_to_my_location": "Мое местоположение",
|
||||
"source_updated": "Map source updated",
|
||||
"failed_set_active": "Failed to set active map",
|
||||
"file_deleted": "File deleted",
|
||||
"failed_delete_file": "Failed to delete file",
|
||||
"storage_saved": "Storage directory saved",
|
||||
"failed_save_storage": "Failed to save directory",
|
||||
"export_cancelled": "Export cancelled",
|
||||
"failed_cancel_export": "Failed to cancel export",
|
||||
"failed_start_export": "Failed to start export",
|
||||
"select_mbtiles_error": "Please select an .mbtiles file",
|
||||
"view_saved": "Default view saved",
|
||||
"failed_save_view": "Failed to save default view",
|
||||
"failed_clear_cache": "Failed to clear cache",
|
||||
"failed_save_tile_server": "Failed to save tile server URL",
|
||||
"failed_save_nominatim": "Failed to save Nominatim API URL",
|
||||
"copied_coordinates": "Copied coordinates",
|
||||
"failed_load_drawings": "Failed to load drawings",
|
||||
"not_initialized": "Map not initialized",
|
||||
"drawing_saved": "Drawing saved",
|
||||
"failed_save_drawing": "Failed to save drawing",
|
||||
"deleted": "Deleted",
|
||||
"failed_delete": "Failed to delete",
|
||||
"location_not_determined": "Could not determine your location",
|
||||
"geolocation_not_supported": "Geolocation is not supported by your browser",
|
||||
"no_nodes_location": "No discovered nodes with location found",
|
||||
"failed_fetch_nodes": "Failed to fetch discovered nodes for mapping"
|
||||
},
|
||||
"interface": {
|
||||
"disable": "Выключить",
|
||||
@@ -454,7 +536,74 @@
|
||||
"generate_paper_message": "Создать бумажное сообщение (LXM)",
|
||||
"recording": "Запись: {duration}",
|
||||
"nomad_network_node": "Узел Nomad Network",
|
||||
"toggle_source": "Исходный код"
|
||||
"toggle_source": "Исходный код",
|
||||
"folder_created": "Folder created",
|
||||
"failed_create_folder": "Failed to create folder",
|
||||
"folder_renamed": "Folder renamed",
|
||||
"failed_rename_folder": "Failed to rename folder",
|
||||
"folder_deleted": "Folder deleted",
|
||||
"failed_delete_folder": "Failed to delete folder",
|
||||
"moved_to_folder": "Moved to folder",
|
||||
"failed_move_folder": "Failed to move to folder",
|
||||
"marked_read": "Marked as read",
|
||||
"failed_mark_read": "Failed to mark as read",
|
||||
"conversations_deleted": "Conversations deleted",
|
||||
"failed_delete_conversations": "Failed to delete conversations",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders imported",
|
||||
"failed_import_folders": "Failed to import folders",
|
||||
"failed_read_clipboard": "Failed to read from clipboard",
|
||||
"failed_send_ingest": "Failed to send ingest request",
|
||||
"address_copied": "Your LXMF address copied to clipboard",
|
||||
"failed_copy_address": "Failed to copy address",
|
||||
"translation_failed": "Translation failed",
|
||||
"failed_add_contact": "Failed to add contact",
|
||||
"ingesting_paper_message": "Ingesting paper message...",
|
||||
"failed_ingest_paper": "Failed to ingest paper message",
|
||||
"enter_display_name": "Enter a custom display name",
|
||||
"failed_update_display_name": "Failed to update display name",
|
||||
"failed_load_audio": "Failed to load audio attachment.",
|
||||
"no_contacts_telephone": "No contacts found in telephone",
|
||||
"failed_load_contacts": "Failed to load contacts",
|
||||
"location_request_sent": "Location request sent",
|
||||
"failed_send_location_request": "Failed to send location request",
|
||||
"remove_image_confirm": "Are you sure you want to remove this image attachment?",
|
||||
"failed_start_recording": "failed to start recording",
|
||||
"remove_audio_confirm": "Are you sure you want to remove this audio attachment?",
|
||||
"failed_generate_qr": "Failed to generate QR",
|
||||
"enter_folder_name": "Enter folder name",
|
||||
"new_folder": "New Folder",
|
||||
"rename_folder": "Rename folder",
|
||||
"failed_render_qr": "Failed to render QR code",
|
||||
"uri_copied": "URI copied to clipboard",
|
||||
"failed_copy_uri": "Failed to copy URI",
|
||||
"paper_message_sent": "Paper message sent successfully",
|
||||
"failed_send_paper": "Failed to send paper message",
|
||||
"failed_load_config": "Failed to load configuration",
|
||||
"profile_icon_saved": "Profile icon saved successfully",
|
||||
"failed_save_profile_icon": "Failed to save profile icon",
|
||||
"select_colors_warning": "Please select both background and icon colors",
|
||||
"select_icon_warning": "Please select an icon",
|
||||
"changes_reset": "Changes reset to saved values",
|
||||
"profile_icon_removed": "Profile icon removed successfully",
|
||||
"user_banished": "User banished successfully",
|
||||
"failed_banish_user": "Failed to banish user",
|
||||
"delete_conversation_confirm": "Are you sure you want to delete this conversation?",
|
||||
"failed_delete_conversation": "failed to delete conversation",
|
||||
"delete_history_confirm": "Are you sure you want to delete all messages in this conversation? This can not be undone!",
|
||||
"failed_delete_history": "failed to delete history",
|
||||
"invalid_destination_hash": "Invalid destination hash",
|
||||
"invalid_destination_hash_format": "Invalid destination hash format",
|
||||
"ping_failed": "Ping failed. Try again later",
|
||||
"ping_reply_from": "Valid reply from {hash}",
|
||||
"duration": "Duration: {duration}",
|
||||
"hops_there": "Hops There: {count}",
|
||||
"hops_back": "Hops Back: {count}",
|
||||
"signal_quality": "Signal Quality: {quality}%",
|
||||
"rssi_val": "RSSI: {rssi}dBm",
|
||||
"snr_val": "SNR: {snr}dB",
|
||||
"hash_copied": "Identity hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy identity hash"
|
||||
},
|
||||
"nomadnet": {
|
||||
"remove_favourite": "Удалить из избранного",
|
||||
@@ -494,7 +643,16 @@
|
||||
"enter_nomadnet_url": "Введите URL Nomadnet",
|
||||
"archiving_page": "Архивация страницы...",
|
||||
"page_archived_successfully": "Страница успешно архивирована.",
|
||||
"identify_confirm": "Вы уверены, что хотите идентифицировать себя для этого узла NomadNetwork? Страница будет перезагружена после отправки вашей личности."
|
||||
"identify_confirm": "Вы уверены, что хотите идентифицировать себя для этого узла NomadNetwork? Страница будет перезагружена после отправки вашей личности.",
|
||||
"failed_rename_favourite": "Failed to rename favourite",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment",
|
||||
"enter_section_name": "Enter section name",
|
||||
"new_section": "New Section",
|
||||
"rename_section": "Rename section",
|
||||
"delete_section_confirm": "Delete this section? Favourites will move to the main section.",
|
||||
"unsupported_url": "unsupported url: ",
|
||||
"no_search_results_peers": "No peers found matching your search."
|
||||
},
|
||||
"forwarder": {
|
||||
"title": "LXMF Форвардер",
|
||||
@@ -537,7 +695,10 @@
|
||||
"status_not_downloaded": "Не загружено",
|
||||
"btn_download": "Скачать руководства",
|
||||
"btn_update": "Обновить руководство",
|
||||
"error": "Ошибка"
|
||||
"error": "Ошибка",
|
||||
"failed_update_docs": "Failed to update documentation sources",
|
||||
"docs_link_copied": "Documentation link copied to clipboard",
|
||||
"failed_copy_link": "Failed to copy link"
|
||||
},
|
||||
"tools": {
|
||||
"utilities": "Утилиты",
|
||||
@@ -561,7 +722,17 @@
|
||||
},
|
||||
"rnpath": {
|
||||
"title": "RNPath",
|
||||
"description": "Управление таблицей путей, просмотр частоты анонсов и запрос путей."
|
||||
"description": "Управление таблицей путей, просмотр частоты анонсов и запрос путей.",
|
||||
"failed_fetch": "Failed to fetch path data",
|
||||
"path_dropped": "Path dropped",
|
||||
"failed_drop": "Could not drop path",
|
||||
"error_drop": "Error dropping path",
|
||||
"failed_request": "Failed to request path",
|
||||
"paths_dropped": "Paths dropped",
|
||||
"failed_drop_paths": "Failed to drop paths",
|
||||
"purge_confirm": "Purge all announce queues? This cannot be undone.",
|
||||
"queues_purged": "Announce queues purged",
|
||||
"failed_purge": "Failed to purge queues"
|
||||
},
|
||||
"translator": {
|
||||
"title": "Translator",
|
||||
@@ -890,8 +1061,11 @@
|
||||
"available_languages": "Доступные языки",
|
||||
"languages_loaded_from": "Языки загружаются из LibreTranslate API или пакетов Argos Translate.",
|
||||
"refresh_languages": "Обновить языки",
|
||||
"failed_to_load_languages": "Не удалось загрузить языки. Убедитесь, что LibreTranslate запущен или Argos Translate установлен.",
|
||||
"copied_to_clipboard": "Скопировано в буфер обмена"
|
||||
"copied_to_clipboard": "Скопировано в буфер обмена",
|
||||
"failed_load_languages": "Failed to load languages. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"failed_translate": "Translation failed. Make sure LibreTranslate is running or Argos Translate is installed.",
|
||||
"select_languages_warning": "Please select both source and target languages.",
|
||||
"auto_detect_not_supported": "Auto-detection is not supported with Argos Translate. Please select a source language."
|
||||
},
|
||||
"call": {
|
||||
"phone": "Телефон",
|
||||
@@ -1012,7 +1186,36 @@
|
||||
"failed_to_set_primary_ringtone": "Не удалось установить основной рингтон",
|
||||
"failed_to_update_ringtone_name": "Не удалось обновить название рингтона",
|
||||
"failed_to_play_ringtone": "Не удалось воспроизвести рингтон",
|
||||
"failed_to_play_voicemail": "Не удалось воспроизвести сообщение"
|
||||
"failed_to_play_voicemail": "Не удалось воспроизвести сообщение",
|
||||
"web_audio_not_available": "Web audio not available",
|
||||
"failed_to_update_dnd": "Failed to update Do Not Disturb status",
|
||||
"failed_to_update_call_settings": "Failed to update call settings",
|
||||
"failed_to_update_recording_status": "Failed to update call recording status",
|
||||
"call_history_cleared": "Call history cleared",
|
||||
"failed_to_clear_call_history": "Failed to clear call history",
|
||||
"identity_banished": "Identity banished",
|
||||
"failed_to_banish_identity": "Failed to banish identity",
|
||||
"name_and_hash_required": "Name and identity hash required",
|
||||
"contact_updated": "Contact updated",
|
||||
"contact_added": "Contact added",
|
||||
"contact_deleted": "Contact deleted",
|
||||
"failed_to_delete_contact": "Failed to delete contact",
|
||||
"hash_copied": "Hash copied to clipboard",
|
||||
"failed_to_copy_hash": "Failed to copy hash",
|
||||
"greeting_uploaded_successfully": "Greeting uploaded successfully",
|
||||
"greeting_deleted": "Greeting deleted",
|
||||
"failed_to_delete_greeting": "Failed to delete greeting",
|
||||
"failed_to_start_recording_greeting": "Failed to start recording greeting",
|
||||
"greeting_recorded_from_mic": "Greeting recorded from mic",
|
||||
"failed_to_stop_recording_greeting": "Failed to stop recording greeting",
|
||||
"failed_to_load_recording": "Failed to load recording audio",
|
||||
"recording_deleted": "Recording deleted",
|
||||
"failed_to_delete_recording": "Failed to delete recording",
|
||||
"call_sent_to_voicemail": "Call sent to voicemail",
|
||||
"failed_to_send_to_voicemail": "Failed to send call to voicemail",
|
||||
"failed_load_audio_edit": "Failed to load audio for editing",
|
||||
"ringtone_saved": "Ringtone saved successfully",
|
||||
"failed_save_ringtone": "Failed to save edited ringtone"
|
||||
},
|
||||
"tutorial": {
|
||||
"title": "Начало работы",
|
||||
@@ -1091,7 +1294,9 @@
|
||||
"discovery_question": "Вы хотите использовать обнаружение интерфейсов сообщества и автоподключение?",
|
||||
"discovery_desc": "Это позволяет MeshChatX автоматически находить и подключаться к публичным узлам сообщества рядом с вами или в интернете.",
|
||||
"yes": "Да, использовать обнаружение",
|
||||
"no": "Нет, ручная настройка"
|
||||
"no": "Нет, ручная настройка",
|
||||
"discovery_enabled": "Community discovery enabled",
|
||||
"failed_enable_discovery": "Failed to enable discovery"
|
||||
},
|
||||
"command_palette": {
|
||||
"search_placeholder": "Поиск команд, навигация или поиск узлов...",
|
||||
@@ -1141,9 +1346,100 @@
|
||||
"action_compose_desc": "Начать новое сообщение",
|
||||
"action_orbit": "Переключить Orbit",
|
||||
"action_orbit_desc": "Переключить режим Orbit",
|
||||
"action_bouncing_balls": "Прыгающие шарики",
|
||||
"action_bouncing_balls_desc": "Переключить режим прыгающих шариков",
|
||||
"action_getting_started": "Начало работы",
|
||||
"action_getting_started_desc": "Показать руководство",
|
||||
"action_changelog": "Список изменений",
|
||||
"action_changelog_desc": "Просмотр изменений"
|
||||
},
|
||||
"settings": {
|
||||
"shortcut_saved": "Shortcut saved",
|
||||
"shortcut_deleted": "Shortcut deleted",
|
||||
"archived_pages_flushed": "Archived pages flushed.",
|
||||
"failed_enable_transport": "Failed to enable transport mode!",
|
||||
"failed_disable_transport": "Failed to disable transport mode!",
|
||||
"failed_reload_reticulum": "Failed to reload Reticulum!",
|
||||
"folders_exported": "Folders exported",
|
||||
"failed_export_folders": "Failed to export folders",
|
||||
"folders_imported": "Folders and mappings imported successfully",
|
||||
"failed_import_folders": "Failed to import folders"
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug Logs",
|
||||
"description": "View application logs and detected anomalies.",
|
||||
"failed_fetch_logs": "Failed to fetch logs",
|
||||
"logs_copied": "Logs on this page copied to clipboard",
|
||||
"failed_copy_logs": "Failed to copy logs"
|
||||
},
|
||||
"visualiser": {
|
||||
"title": "Network Visualiser",
|
||||
"description": "Visualize the mesh network topology.",
|
||||
"reticulum_mesh": "Reticulum Mesh",
|
||||
"network_visualizer": "Network Visualizer",
|
||||
"node_info": "Node Info",
|
||||
"hash": "Hash",
|
||||
"hops": "Hops",
|
||||
"interface": "Interface",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"first_seen": "First seen",
|
||||
"last_seen": "Last seen",
|
||||
"center_node": "Center on node",
|
||||
"copy_hash": "Copy Hash",
|
||||
"banish_identity": "Banish identity",
|
||||
"banished_identities": "Banished identities",
|
||||
"filter_nodes": "Filter nodes...",
|
||||
"all": "All",
|
||||
"direct": "Direct",
|
||||
"one_hop": "1 Hop",
|
||||
"multi_hops": "2+ Hops",
|
||||
"clear": "Clear",
|
||||
"show_labels": "Show all labels",
|
||||
"bouncing_balls": "Bouncing balls mode",
|
||||
"orbit_mode": "Orbit mode",
|
||||
"show_edges": "Show edges",
|
||||
"show_tooltips": "Show tooltips",
|
||||
"show_rssi": "Show RSSI",
|
||||
"show_snr": "Show SNR",
|
||||
"show_hops": "Show Hops",
|
||||
"physics": "Physics",
|
||||
"gravity": "Gravity",
|
||||
"spring_length": "Spring length",
|
||||
"spring_strength": "Spring strength",
|
||||
"damping": "Damping",
|
||||
"central_gravity": "Central gravity",
|
||||
"edge_strength": "Edge strength",
|
||||
"force_atlas_2": "Force Atlas 2",
|
||||
"barns_hut": "Barns Hut",
|
||||
"repulsion": "Repulsion",
|
||||
"reset_physics": "Reset physics",
|
||||
"reset_camera": "Reset Camera",
|
||||
"clear_graph": "Clear graph",
|
||||
"total_nodes": "Total nodes",
|
||||
"total_edges": "Total edges",
|
||||
"discovered": "Discovered",
|
||||
"wait_between_batches": "Wait between batches",
|
||||
"batch": "Batch",
|
||||
"loading_status": "Loading status",
|
||||
"failed_load": "Failed to load network data"
|
||||
},
|
||||
"banishment": {
|
||||
"title": "Banished",
|
||||
"description": "Manage Banished users and nodes",
|
||||
"failed_load_banished": "Failed to load banished destinations",
|
||||
"search_placeholder": "Search by hash or display name...",
|
||||
"loading_items": "Loading banished items...",
|
||||
"no_items": "No banished items found",
|
||||
"lift_banishment": "Lift Banishment",
|
||||
"lift_banishment_confirm": "Are you sure you want to lift the banishment for {name}?",
|
||||
"banished_at": "Banished at",
|
||||
"banished_until": "Banished until",
|
||||
"reason": "Reason",
|
||||
"type": "Type",
|
||||
"user": "User",
|
||||
"node": "Node",
|
||||
"banishment_lifted": "Banishment lifted successfully",
|
||||
"failed_lift_banishment": "Failed to lift banishment"
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,17 @@ describe("BlockedPage.vue (Banished UI)", () => {
|
||||
return mount(BlockedPage, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$t: (key) => {
|
||||
const translations = {
|
||||
"banishment.title": "Banished",
|
||||
"banishment.description": "Manage Banished users and nodes",
|
||||
"banishment.lift_banishment": "Lift Banishment",
|
||||
"banishment.user": "User",
|
||||
"banishment.node": "Node",
|
||||
"banishment.banished_at": "Banished at",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
MaterialDesignIcon: {
|
||||
|
||||
@@ -2,6 +2,15 @@ import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import MicronEditorPage from "@/components/micron-editor/MicronEditorPage.vue";
|
||||
import { micronStorage } from "@/js/MicronStorage";
|
||||
import DialogUtils from "@/js/DialogUtils";
|
||||
|
||||
// Mock DialogUtils
|
||||
vi.mock("@/js/DialogUtils", () => ({
|
||||
default: {
|
||||
confirm: vi.fn().mockResolvedValue(true),
|
||||
alert: vi.fn().mockResolvedValue(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock micronStorage
|
||||
vi.mock("@/js/MicronStorage", () => ({
|
||||
@@ -127,8 +136,10 @@ describe("MicronEditorPage.vue", () => {
|
||||
// Find reset button
|
||||
const resetButton = wrapper.find('.mdi-stub[data-icon-name="refresh"]').element.parentElement;
|
||||
await resetButton.click();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick(); // Wait for async resetAll to complete
|
||||
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
expect(DialogUtils.confirm).toHaveBeenCalled();
|
||||
expect(micronStorage.clearAll).toHaveBeenCalled();
|
||||
expect(wrapper.vm.tabs.length).toBe(2); // Resets to Main + Guide
|
||||
expect(wrapper.vm.activeTabIndex).toBe(0);
|
||||
|
||||
@@ -125,6 +125,16 @@ describe("NetworkVisualiser.vue", () => {
|
||||
const mountVisualiser = () => {
|
||||
return mount(NetworkVisualiser, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (msg) => {
|
||||
const translations = {
|
||||
"visualiser.reticulum_mesh": "Reticulum Mesh",
|
||||
"visualiser.total_nodes": "Nodes",
|
||||
"visualiser.total_edges": "Links",
|
||||
};
|
||||
return translations[msg] || msg;
|
||||
},
|
||||
},
|
||||
stubs: {
|
||||
Toggle: {
|
||||
template:
|
||||
|
||||
@@ -10,6 +10,9 @@ describe("Toast.vue", () => {
|
||||
vi.useFakeTimers();
|
||||
wrapper = mount(Toast, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (msg) => msg,
|
||||
},
|
||||
stubs: {
|
||||
TransitionGroup: { template: "<div><slot /></div>" },
|
||||
MaterialDesignIcon: {
|
||||
|
||||
Reference in New Issue
Block a user