feat(ui): enhance sidebar functionality with collapsible feature, improve styling, and add hash copy functionality across components
This commit is contained in:
@@ -116,22 +116,39 @@
|
||||
|
||||
<!-- sidebar -->
|
||||
<div
|
||||
class="fixed inset-y-0 left-0 z-[70] w-72 transform transition-transform duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
|
||||
:class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||
class="fixed inset-y-0 left-0 z-[70] transform transition-all duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
|
||||
:class="[
|
||||
isSidebarOpen ? 'translate-x-0' : '-translate-x-full',
|
||||
isSidebarCollapsed ? 'w-20' : 'w-72',
|
||||
]"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<!-- toggle button for desktop -->
|
||||
<div class="hidden sm:flex justify-end p-2 border-b border-gray-100 dark:border-zinc-800">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 dark:text-zinc-400 dark:hover:bg-zinc-800 transition-colors"
|
||||
@click="isSidebarCollapsed = !isSidebarCollapsed"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
:icon-name="isSidebarCollapsed ? 'chevron-right' : 'chevron-left'"
|
||||
class="size-5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- navigation -->
|
||||
<div class="flex-1">
|
||||
<ul class="py-3 pr-2 space-y-1">
|
||||
<!-- messages -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'messages' }">
|
||||
<SidebarLink :to="{ name: 'messages' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon
|
||||
icon-name="message-text"
|
||||
class="w-6 h-6 dark:text-white"
|
||||
class="w-6 h-6 text-gray-700 dark:text-white"
|
||||
/>
|
||||
</template>
|
||||
<template #text>
|
||||
@@ -145,9 +162,12 @@
|
||||
|
||||
<!-- nomad network -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'nomadnetwork' }">
|
||||
<SidebarLink :to="{ name: 'nomadnetwork' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="earth" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="earth"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.nomad_network") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -155,9 +175,12 @@
|
||||
|
||||
<!-- map -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'map' }">
|
||||
<SidebarLink :to="{ name: 'map' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="map" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="map"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.map") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -165,9 +188,12 @@
|
||||
|
||||
<!-- archives -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'archives' }">
|
||||
<SidebarLink :to="{ name: 'archives' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="archive" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="archive"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.archives") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -175,9 +201,12 @@
|
||||
|
||||
<!-- telephone -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'call' }">
|
||||
<SidebarLink :to="{ name: 'call' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="phone" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="phone"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.audio_calls") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -185,9 +214,12 @@
|
||||
|
||||
<!-- interfaces -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'interfaces' }">
|
||||
<SidebarLink :to="{ name: 'interfaces' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="router" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="router"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.interfaces") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -195,9 +227,15 @@
|
||||
|
||||
<!-- network visualiser -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'network-visualiser' }">
|
||||
<SidebarLink
|
||||
:to="{ name: 'network-visualiser' }"
|
||||
:is-collapsed="isSidebarCollapsed"
|
||||
>
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="diagram-projector" class="w-6 h-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="diagram-projector"
|
||||
class="w-6 h-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.network_visualiser") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -205,9 +243,12 @@
|
||||
|
||||
<!-- tools -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'tools' }">
|
||||
<SidebarLink :to="{ name: 'tools' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="wrench" class="size-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="wrench"
|
||||
class="size-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.tools") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -215,9 +256,12 @@
|
||||
|
||||
<!-- settings -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'settings' }">
|
||||
<SidebarLink :to="{ name: 'settings' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="cog" class="size-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="cog"
|
||||
class="size-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.settings") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -225,9 +269,12 @@
|
||||
|
||||
<!-- identities -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'identities' }">
|
||||
<SidebarLink :to="{ name: 'identities' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="account-multiple" class="size-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="account-multiple"
|
||||
class="size-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.identities") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -235,9 +282,12 @@
|
||||
|
||||
<!-- info -->
|
||||
<li>
|
||||
<SidebarLink :to="{ name: 'about' }">
|
||||
<SidebarLink :to="{ name: 'about' }" :is-collapsed="isSidebarCollapsed">
|
||||
<template #icon>
|
||||
<MaterialDesignIcon icon-name="information" class="size-6" />
|
||||
<MaterialDesignIcon
|
||||
icon-name="information"
|
||||
class="size-6 text-gray-700 dark:text-gray-200"
|
||||
/>
|
||||
</template>
|
||||
<template #text>{{ $t("app.about") }}</template>
|
||||
</SidebarLink>
|
||||
@@ -255,7 +305,7 @@
|
||||
class="flex text-gray-700 p-3 cursor-pointer"
|
||||
@click="isShowingMyIdentitySection = !isShowingMyIdentitySection"
|
||||
>
|
||||
<div class="my-auto mr-2">
|
||||
<div class="my-auto mr-2 shrink-0">
|
||||
<RouterLink :to="{ name: 'profile.icon' }" @click.stop>
|
||||
<LxmfUserIcon
|
||||
:icon-name="config?.lxmf_user_icon_name"
|
||||
@@ -264,8 +314,10 @@
|
||||
/>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="my-auto dark:text-white">{{ $t("app.my_identity") }}</div>
|
||||
<div class="my-auto ml-auto">
|
||||
<div v-if="!isSidebarCollapsed" class="my-auto dark:text-white truncate">
|
||||
{{ $t("app.my_identity") }}
|
||||
</div>
|
||||
<div v-if="!isSidebarCollapsed" class="my-auto ml-auto shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
|
||||
@@ -276,7 +328,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isShowingMyIdentitySection"
|
||||
v-if="isShowingMyIdentitySection && !isSidebarCollapsed"
|
||||
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
|
||||
>
|
||||
<div class="p-2">
|
||||
@@ -287,19 +339,19 @@
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-zinc-800 dark:border-zinc-600 dark:text-zinc-200 dark:focus:ring-blue-400 dark:focus:border-blue-400"
|
||||
/>
|
||||
</div>
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden">
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
|
||||
<div>{{ $t("app.identity_hash") }}</div>
|
||||
<div
|
||||
class="text-sm text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
:title="config.identity_hash"
|
||||
>
|
||||
{{ config.identity_hash }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden">
|
||||
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
|
||||
<div>{{ $t("app.lxmf_address") }}</div>
|
||||
<div
|
||||
class="text-sm text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono"
|
||||
:title="config.lxmf_address_hash"
|
||||
>
|
||||
{{ config.lxmf_address_hash }}
|
||||
@@ -317,11 +369,13 @@
|
||||
class="flex text-gray-700 p-3 cursor-pointer dark:text-white"
|
||||
@click="isShowingAnnounceSection = !isShowingAnnounceSection"
|
||||
>
|
||||
<div class="my-auto mr-2">
|
||||
<div class="my-auto mr-2 shrink-0">
|
||||
<MaterialDesignIcon icon-name="radio" class="size-6" />
|
||||
</div>
|
||||
<div class="my-auto">{{ $t("app.announce") }}</div>
|
||||
<div class="ml-auto">
|
||||
<div v-if="!isSidebarCollapsed" class="my-auto truncate">
|
||||
{{ $t("app.announce") }}
|
||||
</div>
|
||||
<div v-if="!isSidebarCollapsed" class="ml-auto shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
|
||||
@@ -332,7 +386,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isShowingAnnounceSection"
|
||||
v-if="isShowingAnnounceSection && !isSidebarCollapsed"
|
||||
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
|
||||
>
|
||||
<div class="p-2 dark:border-zinc-800">
|
||||
@@ -350,7 +404,7 @@
|
||||
<option value="43200">Every 12 Hours</option>
|
||||
<option value="86400">Every 24 Hours</option>
|
||||
</select>
|
||||
<div class="text-sm text-gray-700 dark:text-zinc-100">
|
||||
<div class="text-[10px] text-gray-700 dark:text-zinc-100 mt-1">
|
||||
<span v-if="config.last_announced_at">{{
|
||||
$t("app.last_announced", {
|
||||
time: formatSecondsAgo(config.last_announced_at),
|
||||
@@ -365,14 +419,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isPopoutMode" class="flex flex-1 min-w-0 overflow-hidden">
|
||||
<div class="flex flex-1 min-w-0 overflow-hidden">
|
||||
<RouterView class="flex-1 min-w-0 h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<CallOverlay
|
||||
v-if="activeCall || isCallEnded || wasDeclined"
|
||||
v-if="(activeCall || isCallEnded || wasDeclined) && $route.name !== 'call'"
|
||||
:active-call="activeCall || lastCall"
|
||||
:is-ended="isCallEnded"
|
||||
:was-declined="wasDeclined"
|
||||
@@ -443,6 +497,7 @@ export default {
|
||||
isShowingAnnounceSection: true,
|
||||
|
||||
isSidebarOpen: false,
|
||||
isSidebarCollapsed: false,
|
||||
|
||||
isSwitchingIdentity: false,
|
||||
|
||||
@@ -496,6 +551,13 @@ export default {
|
||||
if (newConfig && newConfig.custom_ringtone_enabled !== undefined) {
|
||||
this.updateRingtonePlayer();
|
||||
}
|
||||
if (newConfig && newConfig.theme) {
|
||||
if (newConfig.theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
@@ -551,6 +613,13 @@ export default {
|
||||
case "config": {
|
||||
this.config = json.config;
|
||||
this.displayName = json.config.display_name;
|
||||
if (this.config?.theme) {
|
||||
if (this.config.theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "announced": {
|
||||
@@ -559,6 +628,9 @@ export default {
|
||||
break;
|
||||
}
|
||||
case "telephone_ringing": {
|
||||
if (this.config?.do_not_disturb_enabled) {
|
||||
break;
|
||||
}
|
||||
NotificationUtils.showIncomingCallNotification();
|
||||
this.updateTelephoneStatus();
|
||||
this.playRingtone();
|
||||
@@ -616,6 +688,13 @@ export default {
|
||||
try {
|
||||
const response = await window.axios.get(`/api/v1/config`);
|
||||
this.config = response.data.config;
|
||||
if (this.config?.theme) {
|
||||
if (this.config.theme === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
@@ -817,6 +896,11 @@ export default {
|
||||
this.wasDeclined = false;
|
||||
this.lastCall = null;
|
||||
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
||||
} else if (!this.endedTimeout) {
|
||||
// If no call and no ended state timeout active, ensure everything is reset
|
||||
this.isCallEnded = false;
|
||||
this.wasDeclined = false;
|
||||
this.lastCall = null;
|
||||
}
|
||||
} catch {
|
||||
// do nothing on error
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
? 'bg-blue-100 text-blue-800 group:text-blue-800 dark:bg-zinc-800 dark:text-blue-300'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-zinc-700',
|
||||
]"
|
||||
class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500"
|
||||
class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"
|
||||
@click="handleNavigate($event, navigate)"
|
||||
>
|
||||
<span class="my-auto">
|
||||
<span class="my-auto shrink-0">
|
||||
<slot name="icon"></slot>
|
||||
</span>
|
||||
<span class="my-auto flex w-full">
|
||||
<span v-if="!isCollapsed" class="my-auto flex w-full truncate">
|
||||
<slot name="text"></slot>
|
||||
</span>
|
||||
</a>
|
||||
@@ -29,6 +29,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
methods: {
|
||||
|
||||
@@ -3,33 +3,35 @@
|
||||
<div class="flex flex-col flex-1 h-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
|
||||
<!-- header -->
|
||||
<div
|
||||
class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-sm"
|
||||
class="flex flex-col sm:flex-row sm:items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-sm gap-4"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg shrink-0">
|
||||
<MaterialDesignIcon icon-name="archive" class="size-6 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{{ $t("app.archives") }}</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t("archives.description") }}</p>
|
||||
<div class="min-w-0">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{{ $t("app.archives") }}</h1>
|
||||
<p class="text-xs sm:text-sm text-gray-500 dark:text-gray-400 truncate">
|
||||
{{ $t("archives.description") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2 sm:gap-4">
|
||||
<div class="relative w-32 sm:w-64 md:w-80">
|
||||
<div class="flex items-center gap-2 sm:ml-auto">
|
||||
<div class="relative flex-1 sm:w-64 md:w-80">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
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"
|
||||
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 text-sm"
|
||||
:placeholder="$t('archives.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"
|
||||
class="p-2 text-gray-500 hover:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 transition-colors shrink-0"
|
||||
:title="$t('common.refresh')"
|
||||
@click="getArchives"
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,7 +56,13 @@
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-400 mt-0.5">
|
||||
<!-- destination hash -->
|
||||
<div class="inline-block mr-1">
|
||||
<div><{{ selectedPeer.destination_hash }}></div>
|
||||
<div
|
||||
class="cursor-pointer hover:text-blue-500 transition-colors"
|
||||
:title="selectedPeer.destination_hash"
|
||||
@click="copyHash(selectedPeer.destination_hash)"
|
||||
>
|
||||
{{ formatDestinationHash(selectedPeer.destination_hash) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inline-block">
|
||||
@@ -757,7 +763,7 @@
|
||||
<div class="flex flex-wrap gap-2 items-center mt-2">
|
||||
<button type="button" class="attachment-action-button" @click="addFilesToMessage">
|
||||
<MaterialDesignIcon icon-name="paperclip-plus" class="w-4 h-4" />
|
||||
<span>{{ $t("messages.add_files") }}</span>
|
||||
<span class="hidden sm:inline">{{ $t("messages.add_files") }}</span>
|
||||
</button>
|
||||
<AddImageButton @add-image="onImageSelected" />
|
||||
<AddAudioButton
|
||||
@@ -774,7 +780,7 @@
|
||||
@click="shareLocation"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="map-marker" class="w-4 h-4" />
|
||||
<span>{{ $t("messages.location") }}</span>
|
||||
<span class="hidden sm:inline">{{ $t("messages.location") }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -783,7 +789,7 @@
|
||||
@click="requestLocation"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="crosshairs-question" class="w-4 h-4" />
|
||||
<span>{{ $t("messages.request") }}</span>
|
||||
<span class="hidden sm:inline">{{ $t("messages.request") }}</span>
|
||||
</button>
|
||||
<div class="ml-auto my-auto">
|
||||
<SendMessageButton
|
||||
@@ -1951,6 +1957,18 @@ export default {
|
||||
formatTimeAgo: function (datetimeString) {
|
||||
return Utils.formatTimeAgo(datetimeString);
|
||||
},
|
||||
formatDestinationHash(hash) {
|
||||
return Utils.formatDestinationHash(hash);
|
||||
},
|
||||
async copyHash(hash) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
ToastUtils.success("Hash copied to clipboard");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
ToastUtils.error("Failed to copy hash");
|
||||
}
|
||||
},
|
||||
formatBytes: function (bytes) {
|
||||
return Utils.formatBytes(bytes);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
|
||||
<!-- Compact Header -->
|
||||
<div
|
||||
class="flex items-center justify-between px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm shrink-0"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="bg-teal-100 dark:bg-teal-900/30 p-1.5 rounded-xl shrink-0">
|
||||
<MaterialDesignIcon icon-name="code-tags" class="size-5 text-teal-600 dark:text-teal-400" />
|
||||
</div>
|
||||
<h1
|
||||
class="text-sm font-bold text-gray-900 dark:text-white uppercase tracking-wider hidden sm:block truncate"
|
||||
>
|
||||
{{ $t("tools.micron_editor.title") }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" class="secondary-chip !py-1 !px-3" @click="downloadFile">
|
||||
<MaterialDesignIcon icon-name="download" class="w-3.5 h-3.5" />
|
||||
<span class="hidden sm:inline">{{ $t("tools.micron_editor.download") }}</span>
|
||||
</button>
|
||||
<button v-if="isMobileView" type="button" class="primary-chip !py-1 !px-3" @click="toggleView">
|
||||
<MaterialDesignIcon :icon-name="showEditor ? 'eye' : 'pencil'" class="w-3.5 h-3.5" />
|
||||
{{ showEditor ? $t("tools.micron_editor.view_preview") : $t("tools.micron_editor.edit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Editor Pane -->
|
||||
<div
|
||||
:class="[
|
||||
'flex-1 overflow-hidden flex flex-col',
|
||||
isMobileView && !showEditor ? 'hidden' : '',
|
||||
!isMobileView ? 'border-r border-gray-200 dark:border-zinc-800' : '',
|
||||
]"
|
||||
>
|
||||
<textarea
|
||||
ref="editorRef"
|
||||
v-model="content"
|
||||
class="flex-1 w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-sm resize-none focus:outline-none"
|
||||
:placeholder="$t('tools.micron_editor.placeholder')"
|
||||
@input="handleInput"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Preview Pane -->
|
||||
<div
|
||||
:class="[
|
||||
'flex-1 overflow-hidden flex flex-col bg-slate-100 dark:bg-black',
|
||||
isMobileView && showEditor ? 'hidden' : '',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
ref="previewRef"
|
||||
class="flex-1 overflow-auto text-gray-900 dark:text-white p-4 font-mono text-sm whitespace-pre-wrap break-words nodeContainer"
|
||||
v-html="renderedContent"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import MicronParser from "micron-parser";
|
||||
|
||||
export default {
|
||||
name: "MicronEditorPage",
|
||||
components: {
|
||||
MaterialDesignIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: "",
|
||||
renderedContent: "",
|
||||
showEditor: true,
|
||||
isMobileView: false,
|
||||
storageKey: "micron_editor_content",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadContent();
|
||||
this.handleResize();
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
|
||||
// Listen for theme changes to re-render micron
|
||||
this.themeObserver = new MutationObserver(() => {
|
||||
this.handleInput();
|
||||
});
|
||||
this.themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
this.handleInput();
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
if (this.themeObserver) {
|
||||
this.themeObserver.disconnect();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isDarkMode() {
|
||||
return document.documentElement.classList.contains("dark");
|
||||
},
|
||||
handleResize() {
|
||||
this.isMobileView = window.innerWidth < 1024;
|
||||
if (!this.isMobileView) {
|
||||
this.showEditor = true;
|
||||
}
|
||||
},
|
||||
handleInput() {
|
||||
try {
|
||||
const parser = new MicronParser(this.isDarkMode());
|
||||
this.renderedContent = parser.convertMicronToHtml(this.content);
|
||||
} catch (error) {
|
||||
console.error("Error rendering micron:", error);
|
||||
this.renderedContent = `<p style="color: red;">Error rendering: ${error.message}</p>`;
|
||||
}
|
||||
this.saveContent();
|
||||
},
|
||||
toggleView() {
|
||||
this.showEditor = !this.showEditor;
|
||||
},
|
||||
saveContent() {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, this.content);
|
||||
} catch (error) {
|
||||
console.warn("Failed to save content to localStorage:", error);
|
||||
}
|
||||
},
|
||||
loadContent() {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey);
|
||||
if (saved) {
|
||||
this.content = saved;
|
||||
} else {
|
||||
this.content = this.getDefaultContent();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load content from localStorage:", error);
|
||||
this.content = this.getDefaultContent();
|
||||
}
|
||||
},
|
||||
getDefaultContent() {
|
||||
const b = "`";
|
||||
return `${b}Ffd0
|
||||
${b}=
|
||||
_ _
|
||||
(_) (_)
|
||||
_ __ ___ _ ___ _ __ ___ _ __ ______ _ __ __ _ _ __ ___ ___ _ __ _ ___
|
||||
| '_ \` _ \\| |/ __| '__/ _ \\| '_ \\______| '_ \\ / _\` | '__/ __|/ _ \\ '__| / __|
|
||||
| | | | | | | (__| | | (_) | | | | | |_) | (_| | | \\__ \\\\ __/ |_ | \\__ \\\\
|
||||
|_| |_| |_|_|\\___|_| \\___/|_| |_| | .__/ \\__,_|_| |___/\\___|_(_)| |___/
|
||||
| | _/ |
|
||||
|_| |__/
|
||||
|
||||
${b}=
|
||||
${b}f
|
||||
|
||||
${b}!Welcome to Micron Editor${b}!
|
||||
-
|
||||
Micron is a lightweight, terminal-friendly monospace markdown format used in Reticulum applications.
|
||||
|
||||
${b}!With Micron, you can${b}${b}:
|
||||
|
||||
${b}c Align${b}b
|
||||
|
||||
${b}r text,
|
||||
|
||||
${b}a
|
||||
${b}c
|
||||
set ${b}B005 backgrounds, ${b}b and ${b}*${b} ${b}B777${b}Ffffcombine any number of${b}f${b}b${b}_${b}_ ${b}Ff00f${b}Ff80o${b}Ffd0r${b}F9f0m${b}F0f2a${b}F0fdt${b}F07ft${b}F43fi${b}F70fn${b}Fe0fg ${b}ftags.
|
||||
${b}${b}
|
||||
|
||||
>Getting Started
|
||||
|
||||
Start editing your Micron markup in the editor pane. The preview will update automatically.
|
||||
|
||||
>Formatting
|
||||
|
||||
Text can be ${b}!bold${b}! by using \\${b}!, \\${b}_, and \\${b}*.
|
||||
|
||||
>Colors
|
||||
|
||||
Foreground colors: ${b}Ff00${b}Ff80o${b}Ffd0r${b}F9f0m${b}F0f2a${b}F0fdt${b}F07ft${b}F43fi${b}F70fn${b}Fe0fg${b}f
|
||||
Background colors: ${b}Bf00${b}Bf80o${b}Bfd0r${b}B9f0m${b}B0f2a${b}B0fdt${b}B07ft${b}B43fi${b}B70fn${b}Be0fg${b}b
|
||||
|
||||
>Links
|
||||
|
||||
Create links with \\${b}[ tag: ${b}_${b}[Example Link${b}example.com]${b}]${b}_
|
||||
|
||||
>Literals
|
||||
|
||||
Use \\${b}= to start/end literal blocks that won't be interpreted.
|
||||
|
||||
${b}=
|
||||
This is a literal block
|
||||
${b}=
|
||||
`;
|
||||
},
|
||||
downloadFile() {
|
||||
const blob = new Blob([this.content], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "micron.mu";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.nodeContainer {
|
||||
font-family:
|
||||
Roboto Mono Nerd Font,
|
||||
monospace;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
:deep(.Mu-nl) {
|
||||
cursor: pointer;
|
||||
}
|
||||
:deep(.Mu-mnt) {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
text-align: center;
|
||||
white-space: pre;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
:deep(.Mu-mws) {
|
||||
text-decoration: inherit;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -98,6 +98,19 @@
|
||||
<MaterialDesignIcon icon-name="chevron-right" class="tool-card__chevron" />
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink :to="{ name: 'micron-editor' }" class="tool-card glass-card">
|
||||
<div class="tool-card__icon bg-teal-50 text-teal-500 dark:bg-teal-900/30 dark:text-teal-200">
|
||||
<MaterialDesignIcon icon-name="code-tags" class="w-6 h-6" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="tool-card__title">{{ $t("tools.micron_editor.title") }}</div>
|
||||
<div class="tool-card__description">
|
||||
{{ $t("tools.micron_editor.description") }}
|
||||
</div>
|
||||
</div>
|
||||
<MaterialDesignIcon icon-name="chevron-right" class="tool-card__chevron" />
|
||||
</RouterLink>
|
||||
|
||||
<a target="_blank" href="/rnode-flasher/index.html" class="tool-card glass-card">
|
||||
<div
|
||||
class="tool-card__icon bg-purple-50 text-purple-500 dark:bg-purple-900/30 dark:text-purple-200"
|
||||
|
||||
Reference in New Issue
Block a user