diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index 74bb276..90f72f5 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -66,6 +66,14 @@ + + + +
  • + +
    +
    +

    + + {{ $t("call.ringtone_settings") }} +

    + +
    + +
    +
    +
    + {{ $t("call.enable_custom_ringtone") }} +
    +
    + {{ $t("call.enable_custom_ringtone_description") }} +
    +
    + +
    + + +
    +
    + + + +
    + +
    +
    +
    +
    + +
    +
    + + {{ ringtone.display_name }} + + + Primary + + +
    +
    + {{ ringtone.filename }} +
    +
    + +
    + + + +
    +
    +
    +
    + +
    + {{ $t("call.no_custom_ringtone_uploaded") }} +
    +
    +
    +
    +
    +
    +
    { + mounted() { + this.getConfig(); + this.getAudioProfiles(); this.getStatus(); + this.getHistory(); + this.getVoicemails(); this.getVoicemailStatus(); - }, 1000); + this.getRingtones(); + this.getRingtoneStatus(); + + // poll for status + this.statusInterval = setInterval(() => { + this.getStatus(); + this.getVoicemailStatus(); + this.getRingtoneStatus(); + }, 1000); // poll for history/voicemails less frequently this.historyInterval = setInterval(() => { @@ -786,6 +957,17 @@ export default { console.log(e); } }, + async clearHistory() { + if (!confirm(this.$t("common.delete_confirm"))) return; + try { + await window.axios.delete("/api/v1/telephone/history"); + this.callHistory = []; + ToastUtils.success("Call history cleared"); + } catch (e) { + console.error(e); + ToastUtils.error("Failed to clear call history"); + } + }, async getVoicemailStatus() { try { const response = await window.axios.get("/api/v1/telephone/voicemail/status"); @@ -794,6 +976,115 @@ export default { console.log(e); } }, + async getRingtoneStatus() { + try { + const response = await window.axios.get("/api/v1/telephone/ringtones/status"); + this.ringtoneStatus = response.data; + } catch (e) { + console.log(e); + } + }, + async getRingtones() { + try { + const response = await window.axios.get("/api/v1/telephone/ringtones"); + this.ringtones = response.data; + } catch (e) { + console.error("Failed to get ringtones:", e); + } + }, + async deleteRingtone(ringtone) { + if (!confirm(this.$t("common.delete_confirm"))) return; + try { + await window.axios.delete(`/api/v1/telephone/ringtones/${ringtone.id}`); + ToastUtils.success(this.$t("call.ringtone_deleted")); + await this.getRingtones(); + await this.getRingtoneStatus(); + } catch (e) { + console.error(e); + ToastUtils.error(this.$t("call.failed_to_delete_ringtone")); + } + }, + async setPrimaryRingtone(ringtone) { + try { + await window.axios.patch(`/api/v1/telephone/ringtones/${ringtone.id}`, { + is_primary: true, + }); + ToastUtils.success(this.$t("call.primary_ringtone_set")); + await this.getRingtones(); + await this.getRingtoneStatus(); + } catch (e) { + console.error(e); + ToastUtils.error(this.$t("call.failed_to_set_primary_ringtone")); + } + }, + startEditingRingtone(ringtone) { + this.editingRingtoneId = ringtone.id; + this.editingRingtoneName = ringtone.display_name; + }, + async saveRingtoneName() { + try { + await window.axios.patch(`/api/v1/telephone/ringtones/${this.editingRingtoneId}`, { + display_name: this.editingRingtoneName, + }); + this.editingRingtoneId = null; + await this.getRingtones(); + } catch (e) { + console.error(e); + ToastUtils.error(this.$t("call.failed_to_update_ringtone_name")); + } + }, + async uploadRingtone(event) { + const file = event.target.files[0]; + if (!file) return; + + this.isUploadingRingtone = true; + const formData = new FormData(); + formData.append("file", file); + + try { + await window.axios.post("/api/v1/telephone/ringtones/upload", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + ToastUtils.success(this.$t("call.ringtone_uploaded_successfully")); + await this.getRingtones(); + await this.getRingtoneStatus(); + } catch (e) { + console.error(e); + ToastUtils.error(e.response?.data?.message || this.$t("call.failed_to_upload_ringtone")); + } finally { + this.isUploadingRingtone = false; + event.target.value = ""; + } + }, + async playRingtonePreview(ringtone) { + if (this.isPlayingRingtone && this.playingRingtoneId === ringtone.id) { + this.audioPlayer.pause(); + this.isPlayingRingtone = false; + this.playingRingtoneId = null; + return; + } + + if (this.audioPlayer) { + this.audioPlayer.pause(); + } + + this.playingRingtoneId = ringtone.id; + this.audioPlayer = new Audio(`/api/v1/telephone/ringtones/${ringtone.id}/audio`); + this.audioPlayer.onended = () => { + this.isPlayingRingtone = false; + this.playingRingtoneId = null; + }; + + try { + await this.audioPlayer.play(); + this.isPlayingRingtone = true; + } catch (e) { + console.error(e); + ToastUtils.error(this.$t("call.failed_to_play_ringtone")); + } + }, async getVoicemails() { try { const response = await window.axios.get("/api/v1/telephone/voicemails"); @@ -845,7 +1136,7 @@ export default { await window.axios.delete("/api/v1/telephone/voicemail/greeting"); ToastUtils.success("Greeting deleted"); await this.getVoicemailStatus(); - } catch (e) { + } catch { ToastUtils.error("Failed to delete greeting"); } }, @@ -901,7 +1192,7 @@ export default { this.isPlayingGreeting = true; this.audioPlayer = new Audio("/api/v1/telephone/voicemail/greeting/audio"); this.audioPlayer.play().catch(() => { - ToastUtils.error("No greeting audio found. Please generate one first."); + ToastUtils.error(this.$t("call.no_greeting_audio_found")); this.isPlayingGreeting = false; }); this.audioPlayer.onended = () => { diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue index 20f574e..c8f3b2e 100644 --- a/meshchatx/src/frontend/components/map/MapPage.vue +++ b/meshchatx/src/frontend/components/map/MapPage.vue @@ -638,7 +638,7 @@ import XYZ from "ol/source/XYZ"; import VectorSource from "ol/source/Vector"; import Feature from "ol/Feature"; import Point from "ol/geom/Point"; -import { Style, Icon, Text, Fill, Stroke, Circle as CircleStyle } from "ol/style"; +import { Style, Text, Fill, Stroke, Circle as CircleStyle } from "ol/style"; import { fromLonLat, toLonLat } from "ol/proj"; import { defaults as defaultControls } from "ol/control"; import DragBox from "ol/interaction/DragBox"; diff --git a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue new file mode 100644 index 0000000..79c4224 --- /dev/null +++ b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue @@ -0,0 +1,312 @@ + + + + +