feat(ui): update user interface with improved icon handling, audio permission checks, and dynamic message icon size adjustments
This commit is contained in:
@@ -85,7 +85,7 @@
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="size-6"
|
||||
:class="{ 'animate-spin-reverse': isSyncingPropagationNode }"
|
||||
:class="{ 'animate-spin': isSyncingPropagationNode }"
|
||||
/>
|
||||
<span class="hidden sm:inline-block my-auto mx-1 text-sm font-medium">{{
|
||||
isSyncingPropagationNode
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
v-if="customImage"
|
||||
class="rounded-full overflow-hidden shrink-0 flex items-center justify-center"
|
||||
:class="iconClass || 'size-6'"
|
||||
:style="iconStyle"
|
||||
>
|
||||
<img :src="customImage" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="iconName"
|
||||
class="p-[10%] rounded-full shrink-0 flex items-center justify-center"
|
||||
:style="{ 'background-color': finalBackgroundColor }"
|
||||
:style="[iconStyle, { 'background-color': finalBackgroundColor }]"
|
||||
:class="iconClass || 'size-6'"
|
||||
>
|
||||
<MaterialDesignIcon :icon-name="iconName" class="size-full" :style="{ color: finalForegroundColor }" />
|
||||
@@ -18,6 +19,7 @@
|
||||
v-else
|
||||
class="bg-gray-100 dark:bg-zinc-800 text-gray-400 dark:text-zinc-500 p-[15%] rounded-full shrink-0 flex items-center justify-center border border-gray-200 dark:border-zinc-700"
|
||||
:class="iconClass || 'size-6'"
|
||||
:style="iconStyle"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account" class="w-full h-full" />
|
||||
</div>
|
||||
@@ -51,6 +53,10 @@ export default {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
iconStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
finalForegroundColor() {
|
||||
|
||||
@@ -2411,18 +2411,26 @@ export default {
|
||||
},
|
||||
async onToggleWebAudio(newVal) {
|
||||
if (!this.config) return;
|
||||
const previousValue = this.config.telephone_web_audio_enabled;
|
||||
this.config.telephone_web_audio_enabled = newVal;
|
||||
try {
|
||||
if (newVal) {
|
||||
const permitted = await this.requestAudioPermission();
|
||||
if (!permitted) {
|
||||
this.config.telephone_web_audio_enabled = false;
|
||||
await this.updateConfig({ telephone_web_audio_enabled: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.updateConfig({ telephone_web_audio_enabled: newVal });
|
||||
if (newVal) {
|
||||
await this.requestAudioPermission();
|
||||
await this.startWebAudio();
|
||||
} else {
|
||||
this.stopWebAudio();
|
||||
}
|
||||
} catch {
|
||||
// revert on failure
|
||||
this.config.telephone_web_audio_enabled = !newVal;
|
||||
this.config.telephone_web_audio_enabled = previousValue;
|
||||
}
|
||||
},
|
||||
async startWebAudio() {
|
||||
@@ -2430,7 +2438,18 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const constraints = this.selectedAudioInputId
|
||||
await this.refreshAudioDevices();
|
||||
const hasInputDevices = (this.audioInputDevices || []).length > 0;
|
||||
if (!hasInputDevices) {
|
||||
ToastUtils.error(this.$t("call.no_audio_input_found"));
|
||||
this.config.telephone_web_audio_enabled = false;
|
||||
await this.updateConfig({ telephone_web_audio_enabled: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const validDeviceIds = new Set((this.audioInputDevices || []).map((d) => d.deviceId));
|
||||
const hasSelectedDevice = this.selectedAudioInputId && validDeviceIds.has(this.selectedAudioInputId);
|
||||
const constraints = hasSelectedDevice
|
||||
? { audio: { deviceId: { exact: this.selectedAudioInputId } } }
|
||||
: { audio: true };
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
@@ -2481,17 +2500,44 @@ export default {
|
||||
this.refreshAudioDevices();
|
||||
} catch (err) {
|
||||
console.error("Web audio failed", err);
|
||||
ToastUtils.error(this.$t("call.web_audio_not_available"));
|
||||
const errorKey =
|
||||
err?.name === "NotFoundError" || err?.name === "OverconstrainedError"
|
||||
? "call.no_audio_input_found"
|
||||
: err?.name === "NotAllowedError"
|
||||
? "call.microphone_permission_denied"
|
||||
: "call.web_audio_not_available";
|
||||
ToastUtils.error(this.$t(errorKey));
|
||||
this.config.telephone_web_audio_enabled = false;
|
||||
await this.updateConfig({ telephone_web_audio_enabled: false });
|
||||
this.stopWebAudio();
|
||||
}
|
||||
},
|
||||
async requestAudioPermission() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
const hasAudioInput = devices.some((d) => d.kind === "audioinput");
|
||||
if (devices.length > 0 && !hasAudioInput) {
|
||||
ToastUtils.error(this.$t("call.no_audio_input_found"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const constraints = this.selectedAudioInputId
|
||||
? { audio: { deviceId: { exact: this.selectedAudioInputId } } }
|
||||
: { audio: true };
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
await this.refreshAudioDevices();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Permission or device request failed", e);
|
||||
const errorKey =
|
||||
e?.name === "NotFoundError" || e?.name === "OverconstrainedError"
|
||||
? "call.no_audio_input_found"
|
||||
: e?.name === "NotAllowedError"
|
||||
? "call.microphone_permission_denied"
|
||||
: "call.web_audio_not_available";
|
||||
ToastUtils.error(this.$t(errorKey));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async refreshAudioDevices() {
|
||||
|
||||
@@ -164,8 +164,8 @@
|
||||
Recently Heard Announces
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Cards appear/disappear as announces are heard. Connected entries show a green
|
||||
pill; disconnected entries are dimmed with a red label.
|
||||
Discovery runs continually; heard announces stay listed. Connected entries show
|
||||
a green pill; disconnected entries are dimmed with a red label.
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -193,7 +193,7 @@
|
||||
v-if="sortedDiscoveredInterfaces.length === 0"
|
||||
class="text-sm text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
No discovered interfaces yet.
|
||||
{{ discoveredEmptyMessage }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -627,6 +627,15 @@ export default {
|
||||
});
|
||||
return set;
|
||||
},
|
||||
discoveredEmptyMessage() {
|
||||
if (!this.isReticulumRunning) {
|
||||
return "LXMF/Reticulum is not running; discovery cannot listen for announces.";
|
||||
}
|
||||
if (!this.discoveryConfig.discover_interfaces) {
|
||||
return "Discovery is disabled. Enable it to start listening for announces.";
|
||||
}
|
||||
return "Discovery is working, be patient while it waits for announces.";
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.reloadInterval);
|
||||
@@ -783,12 +792,40 @@ export default {
|
||||
async loadDiscoveredInterfaces() {
|
||||
try {
|
||||
const response = await window.axios.get(`/api/v1/reticulum/discovered-interfaces`);
|
||||
this.discoveredInterfaces = response.data?.interfaces ?? [];
|
||||
this.discoveredActive = response.data?.active ?? [];
|
||||
const incoming = response.data?.interfaces ?? [];
|
||||
const active = response.data?.active ?? [];
|
||||
|
||||
const merged = new Map();
|
||||
const addOrUpdate = (iface, isNew = false) => {
|
||||
const key = this.discoveryKey(iface);
|
||||
const existing =
|
||||
merged.get(key) || this.discoveredInterfaces.find((i) => this.discoveryKey(i) === key);
|
||||
const lastHeard = iface.last_heard ?? existing?.last_heard ?? Math.floor(Date.now() / 1000);
|
||||
merged.set(key, {
|
||||
...existing,
|
||||
...iface,
|
||||
last_heard: lastHeard,
|
||||
__isNew: isNew || existing?.__isNew,
|
||||
});
|
||||
};
|
||||
|
||||
this.discoveredInterfaces.forEach((iface) => addOrUpdate(iface, false));
|
||||
incoming.forEach((iface) => addOrUpdate(iface, true));
|
||||
|
||||
this.discoveredInterfaces = Array.from(merged.values());
|
||||
this.discoveredActive = active;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
discoveryKey(iface) {
|
||||
return (
|
||||
iface.discovery_hash ||
|
||||
`${iface.reachable_on || iface.target_host || iface.remote || iface.listen_ip || iface.name || "unknown"}:${
|
||||
iface.port || iface.target_port || iface.listen_port || ""
|
||||
}`
|
||||
);
|
||||
},
|
||||
formatLastHeard(ts) {
|
||||
const seconds = Math.max(0, Math.floor(Date.now() / 1000 - ts));
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
:icon-background-colour="
|
||||
selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.background_colour : ''
|
||||
"
|
||||
icon-class="size-11"
|
||||
icon-class="shrink-0"
|
||||
:icon-style="messageIconStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -956,7 +957,7 @@
|
||||
<MaterialDesignIcon
|
||||
icon-name="sync"
|
||||
class="size-6"
|
||||
:class="{ 'animate-spin-reverse': isSyncingPropagationNode }"
|
||||
:class="{ 'animate-spin': isSyncingPropagationNode }"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-gray-900 dark:text-zinc-100">{{
|
||||
@@ -1482,6 +1483,15 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageIconStyle() {
|
||||
const size = Number(this.config?.message_icon_size) || 28;
|
||||
return {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
minWidth: `${size}px`,
|
||||
minHeight: `${size}px`,
|
||||
};
|
||||
},
|
||||
isSyncingPropagationNode() {
|
||||
return [
|
||||
"path_requested",
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
<div v-if="isLoading" class="w-full divide-y divide-gray-100 dark:divide-zinc-800">
|
||||
<div v-for="i in 6" :key="i" class="p-3 animate-pulse">
|
||||
<div class="flex gap-3">
|
||||
<div class="size-10 rounded bg-gray-200 dark:bg-zinc-800"></div>
|
||||
<div class="rounded bg-gray-200 dark:bg-zinc-800" :style="messageIconStyle"></div>
|
||||
<div class="flex-1 space-y-2 py-1">
|
||||
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-3/4"></div>
|
||||
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-1/2"></div>
|
||||
@@ -366,7 +366,8 @@
|
||||
:icon-background-colour="
|
||||
conversation.lxmf_user_icon ? conversation.lxmf_user_icon.background_colour : ''
|
||||
"
|
||||
icon-class="size-7"
|
||||
icon-class="shrink-0"
|
||||
:icon-style="messageIconStyle"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-auto w-full pr-2 min-w-0">
|
||||
@@ -559,7 +560,8 @@
|
||||
:icon-name="peer.lxmf_user_icon?.icon_name"
|
||||
:icon-foreground-colour="peer.lxmf_user_icon?.foreground_colour"
|
||||
:icon-background-colour="peer.lxmf_user_icon?.background_colour"
|
||||
icon-class="size-7"
|
||||
icon-class="shrink-0"
|
||||
:icon-style="messageIconStyle"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
@@ -795,6 +797,10 @@ export default {
|
||||
allSelected() {
|
||||
return this.conversations.length > 0 && this.selectedHashes.size === this.conversations.length;
|
||||
},
|
||||
messageIconStyle() {
|
||||
const size = GlobalState.config?.message_icon_size || 28;
|
||||
return { width: `${size}px`, height: `${size}px` };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
foldersExpanded(newVal) {
|
||||
|
||||
@@ -14,28 +14,20 @@
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $t("app.profile") }}
|
||||
</div>
|
||||
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ config.display_name }}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<input
|
||||
v-model="config.display_name"
|
||||
type="text"
|
||||
:placeholder="$t('app.display_name_placeholder')"
|
||||
class="w-full rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-base font-semibold text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500 outline-none transition"
|
||||
@input="onDisplayNameChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
||||
{{ $t("app.manage_identity") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">{{ $t("app.manage_identity") }}</div>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-4 py-2 text-sm font-semibold text-gray-900 dark:text-zinc-100 shadow-sm hover:border-blue-400 dark:hover:border-blue-400/70 transition"
|
||||
@click="copyValue(config.identity_hash, $t('app.identity_hash'))"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="content-copy" class="w-4 h-4" />
|
||||
{{ $t("app.identity_hash") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-x-2 rounded-xl bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 px-4 py-2 text-sm font-semibold text-white shadow hover:shadow-md transition"
|
||||
@click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="account-plus" class="w-4 h-4" />
|
||||
{{ $t("app.lxmf_address") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
@@ -280,6 +272,22 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-maintenance border-emerald-200 dark:border-emerald-900/30 text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-900/10 hover:bg-emerald-100 dark:hover:bg-emerald-900/20"
|
||||
@click="clearLxmfIcons"
|
||||
>
|
||||
<div class="flex flex-col items-start text-left">
|
||||
<div class="font-bold flex items-center gap-2">
|
||||
<MaterialDesignIcon icon-name="account-off" class="size-4" />
|
||||
{{ $t("maintenance.clear_lxmf_icons") }}
|
||||
</div>
|
||||
<div class="text-xs opacity-80">
|
||||
{{ $t("maintenance.clear_lxmf_icons_desc") }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-maintenance border-blue-200 dark:border-blue-900/30 text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/10 hover:bg-blue-100 dark:hover:bg-blue-900/20"
|
||||
@@ -621,15 +629,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Icon Size</div>
|
||||
<div class="text-xs font-mono text-blue-500 dark:text-blue-400">
|
||||
{{ config.message_icon_size || 28 }}px
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<MaterialDesignIcon icon-name="account-outline" class="text-gray-400" />
|
||||
<input
|
||||
v-model.number="config.message_icon_size"
|
||||
type="range"
|
||||
min="16"
|
||||
max="64"
|
||||
step="1"
|
||||
class="flex-1 h-1.5 bg-gray-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
|
||||
@input="onMessageIconSizeChange"
|
||||
/>
|
||||
<MaterialDesignIcon icon-name="account" class="text-gray-500 dark:text-gray-300" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-300 border border-dashed border-gray-200 dark:border-zinc-800 rounded-2xl px-3 py-2"
|
||||
class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-300 border border-dashed border-gray-200 dark:border-zinc-800 rounded-2xl px-3 py-2"
|
||||
>
|
||||
<div>{{ $t("app.live_preview") }}</div>
|
||||
<div
|
||||
:style="messageIconPreviewStyle"
|
||||
class="flex items-center justify-center shrink-0 rounded-full bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700"
|
||||
>
|
||||
<LxmfUserIcon
|
||||
:key="config.message_icon_size"
|
||||
icon-name="account"
|
||||
icon-class="w-full h-full"
|
||||
icon-foreground-colour="#374151"
|
||||
icon-background-colour="#e5e7eb"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 space-y-0.5">
|
||||
<div
|
||||
class="font-semibold text-gray-900 dark:text-gray-100"
|
||||
:style="previewTextStyle"
|
||||
>
|
||||
Preview Name
|
||||
</div>
|
||||
<div class="text-gray-600 dark:text-gray-400 truncate" :style="previewTextStyle">
|
||||
Hey there, this is how text and icons will look.
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 text-blue-500 dark:text-blue-300 text-xs font-semibold uppercase"
|
||||
>
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
||||
{{ $t("app.realtime") }}
|
||||
{{ $t("app.live_preview") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1168,6 +1220,7 @@ import Toggle from "../forms/Toggle.vue";
|
||||
import ShortcutRecorder from "./ShortcutRecorder.vue";
|
||||
import KeyboardShortcuts from "../../js/KeyboardShortcuts";
|
||||
import ElectronUtils from "../../js/ElectronUtils";
|
||||
import LxmfUserIcon from "../LxmfUserIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "SettingsPage",
|
||||
@@ -1175,6 +1228,7 @@ export default {
|
||||
MaterialDesignIcon,
|
||||
Toggle,
|
||||
ShortcutRecorder,
|
||||
LxmfUserIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -1193,10 +1247,13 @@ export default {
|
||||
lxmf_local_propagation_node_enabled: null,
|
||||
lxmf_preferred_propagation_node_destination_hash: null,
|
||||
archives_max_storage_gb: 1,
|
||||
backup_max_count: 5,
|
||||
banished_effect_enabled: true,
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
blackhole_integration_enabled: true,
|
||||
message_font_size: 14,
|
||||
message_icon_size: 28,
|
||||
telephone_tone_generator_enabled: true,
|
||||
telephone_tone_generator_volume: 50,
|
||||
gitea_base_url: "https://git.quad4.io",
|
||||
@@ -1256,6 +1313,7 @@ export default {
|
||||
"app.light_theme",
|
||||
"app.dark_theme",
|
||||
"Message Font Size",
|
||||
"Icon Size",
|
||||
"app.live_preview",
|
||||
"app.realtime",
|
||||
],
|
||||
@@ -1337,6 +1395,20 @@ export default {
|
||||
}
|
||||
return this.config;
|
||||
},
|
||||
previewTextStyle() {
|
||||
const size = this.config?.message_font_size || 14;
|
||||
return { "font-size": `${size}px` };
|
||||
},
|
||||
messageIconPreviewStyle() {
|
||||
const size = Number(this.config?.message_icon_size) || 28;
|
||||
return {
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
minWidth: `${size}px`,
|
||||
minHeight: `${size}px`,
|
||||
transition: "width 120ms linear, height 120ms linear",
|
||||
};
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
// stop listening for websocket messages
|
||||
@@ -1452,6 +1524,28 @@ export default {
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
async onDisplayNameChange() {
|
||||
if (this.saveTimeouts.display_name) clearTimeout(this.saveTimeouts.display_name);
|
||||
this.saveTimeouts.display_name = setTimeout(async () => {
|
||||
await this.updateConfig(
|
||||
{
|
||||
display_name: this.config.display_name,
|
||||
},
|
||||
"display_name"
|
||||
);
|
||||
}, 600);
|
||||
},
|
||||
async onMessageIconSizeChange() {
|
||||
if (this.saveTimeouts.message_icon_size) clearTimeout(this.saveTimeouts.message_icon_size);
|
||||
this.saveTimeouts.message_icon_size = setTimeout(async () => {
|
||||
await this.updateConfig(
|
||||
{
|
||||
message_icon_size: this.config.message_icon_size,
|
||||
},
|
||||
"message_icon_size"
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
async onLanguageChange() {
|
||||
await this.updateConfig(
|
||||
{
|
||||
@@ -1780,6 +1874,15 @@ export default {
|
||||
ToastUtils.error(this.$t("common.error"));
|
||||
}
|
||||
},
|
||||
async clearLxmfIcons() {
|
||||
if (!(await DialogUtils.confirm(this.$t("maintenance.clear_confirm")))) return;
|
||||
try {
|
||||
await window.axios.delete("/api/v1/maintenance/lxmf-icons");
|
||||
ToastUtils.success(this.$t("maintenance.lxmf_icons_cleared"));
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.error"));
|
||||
}
|
||||
},
|
||||
async clearArchives() {
|
||||
if (!(await DialogUtils.confirm(this.$t("maintenance.clear_confirm")))) return;
|
||||
try {
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<div>
|
||||
<div class="font-bold text-gray-900 dark:text-white">{{ bot.name }}</div>
|
||||
<div class="text-xs font-mono text-gray-500">
|
||||
{{ runningMap[bot.id]?.address || "Not running" }}
|
||||
{{ bot.address || runningMap[bot.id]?.address || "Not running" }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-400">
|
||||
{{ bot.template_id || bot.template }}
|
||||
@@ -277,7 +277,7 @@ export default {
|
||||
try {
|
||||
await window.axios.post("/api/v1/bots/start", {
|
||||
bot_id: bot.id,
|
||||
template_id: bot.template_id,
|
||||
template_id: bot.template_id || bot.template,
|
||||
name: bot.name,
|
||||
});
|
||||
ToastUtils.success(this.$t("bots.bot_started"));
|
||||
|
||||
Reference in New Issue
Block a user