refactor: replace naive-ui with vuetify
This commit is contained in:
@@ -1,39 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NConfigProvider,
|
||||
NGlobalStyle,
|
||||
NMessageProvider,
|
||||
NDialogProvider,
|
||||
darkTheme,
|
||||
} from "naive-ui";
|
||||
import MainLayout from "./components/MainLayout.vue";
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
primaryColor: "#38bdf8",
|
||||
primaryColorHover: "#0ea5e9",
|
||||
},
|
||||
Card: {
|
||||
borderColor: "#334155",
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme="darkTheme" :theme-overrides="themeOverrides">
|
||||
<n-global-style />
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<MainLayout />
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-config-provider>
|
||||
<v-app theme="dark">
|
||||
<MainLayout />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body,
|
||||
#app,
|
||||
.n-config-provider {
|
||||
#app {
|
||||
font-family: "Noto Sans", "Roboto", "Segoe UI", sans-serif !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, computed, h } from "vue";
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import PeerCard from "./PeerCard.vue";
|
||||
import TransferItem from "./TransferItem.vue";
|
||||
import {
|
||||
NLayout,
|
||||
NLayoutHeader,
|
||||
NLayoutContent,
|
||||
NLayoutSider,
|
||||
NSpace,
|
||||
NText,
|
||||
NEmpty,
|
||||
NMenu,
|
||||
NBadge,
|
||||
NButton,
|
||||
NIcon,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faSatelliteDish,
|
||||
faInbox,
|
||||
faBars,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { type MenuOption } from "naive-ui";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service";
|
||||
@@ -32,7 +11,7 @@ import { GetTransferList } from "../../bindings/mesh-drop/internal/transfer/serv
|
||||
const peers = ref<Peer[]>([]);
|
||||
const transferList = ref<Transfer[]>([]);
|
||||
const activeKey = ref("discover");
|
||||
const showMobileMenu = ref(false);
|
||||
const drawer = ref(true); // Control drawer visibility
|
||||
const isMobile = ref(false);
|
||||
|
||||
// 监听窗口大小变化更新 isMobile
|
||||
@@ -43,49 +22,20 @@ onMounted(async () => {
|
||||
transferList.value = (
|
||||
(list || []).filter((t) => t !== null) as Transfer[]
|
||||
).sort((a, b) => b.create_time - a.create_time);
|
||||
|
||||
if (isMobile.value) {
|
||||
drawer.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768;
|
||||
if (!isMobile.value) showMobileMenu.value = false;
|
||||
const mobile = window.innerWidth < 768;
|
||||
if (mobile !== isMobile.value) {
|
||||
isMobile.value = mobile;
|
||||
drawer.value = !mobile;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 菜单选项 ---
|
||||
const renderIcon = (icon: any) => {
|
||||
return () => h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon }) });
|
||||
};
|
||||
|
||||
const menuOptions = computed<MenuOption[]>(() => [
|
||||
{
|
||||
label: "Discover",
|
||||
key: "discover",
|
||||
icon: renderIcon(faSatelliteDish),
|
||||
},
|
||||
{
|
||||
label: () =>
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
style:
|
||||
"display: flex; align-items: center; justify-content: space-between; width: 100%",
|
||||
},
|
||||
[
|
||||
"Transfers",
|
||||
pendingCount.value > 0 ?
|
||||
h(NBadge, {
|
||||
style: "display: inline-flex; align-items: center",
|
||||
value: pendingCount.value,
|
||||
max: 99,
|
||||
type: "error",
|
||||
})
|
||||
: null,
|
||||
],
|
||||
),
|
||||
key: "transfers",
|
||||
icon: renderIcon(faInbox),
|
||||
},
|
||||
]);
|
||||
|
||||
// --- 后端集成 ---
|
||||
onMounted(async () => {
|
||||
peers.value = await GetPeers();
|
||||
@@ -111,146 +61,134 @@ const pendingCount = computed(() => {
|
||||
).length;
|
||||
});
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
title: "Discover",
|
||||
value: "discover",
|
||||
icon: "mdi-radar",
|
||||
},
|
||||
{
|
||||
title: "Transfers",
|
||||
value: "transfers",
|
||||
icon: "mdi-inbox",
|
||||
badge: pendingCount.value > 0 ? pendingCount.value : null,
|
||||
},
|
||||
]);
|
||||
|
||||
// --- 操作 ---
|
||||
|
||||
const handleMenuUpdate = (key: string) => {
|
||||
const handleMenuClick = (key: string) => {
|
||||
activeKey.value = key;
|
||||
showMobileMenu.value = false;
|
||||
if (isMobile.value) {
|
||||
drawer.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 小尺寸头部 -->
|
||||
<n-layout-header v-if="isMobile" bordered class="mobile-header">
|
||||
<n-space
|
||||
align="center"
|
||||
justify="space-between"
|
||||
style="height: 100%; padding: 0 16px">
|
||||
<n-text class="logo">Mesh Drop</n-text>
|
||||
<n-button
|
||||
text
|
||||
style="font-size: 24px"
|
||||
@click="showMobileMenu = !showMobileMenu">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="showMobileMenu ? faXmark : faBars" />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-layout-header>
|
||||
<v-layout>
|
||||
<!-- App Bar for Mobile -->
|
||||
<v-app-bar v-if="isMobile" border flat>
|
||||
<v-toolbar-title class="text-primary font-weight-bold"
|
||||
>Mesh Drop</v-toolbar-title
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-menu" @click="drawer = !drawer"></v-btn>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<!-- 小尺寸抽屉菜单 -->
|
||||
<n-drawer
|
||||
v-model:show="showMobileMenu"
|
||||
placement="top"
|
||||
height="200"
|
||||
v-if="isMobile">
|
||||
<n-drawer-content>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menuOptions"
|
||||
@update:value="handleMenuUpdate" />
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
<n-layout
|
||||
has-sider
|
||||
position="absolute"
|
||||
:style="{ top: isMobile ? '64px' : '0' }">
|
||||
<!-- 桌面端侧边栏 -->
|
||||
<n-layout-sider
|
||||
v-if="!isMobile"
|
||||
bordered
|
||||
width="240"
|
||||
content-style="padding: 24px;">
|
||||
<div class="desktop-logo">
|
||||
<n-text class="logo">Mesh Drop</n-text>
|
||||
<!-- Navigation Drawer -->
|
||||
<v-navigation-drawer v-model="drawer" :permanent="!isMobile">
|
||||
<div class="pa-4" v-if="!isMobile">
|
||||
<div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
|
||||
</div>
|
||||
<n-menu
|
||||
:value="activeKey"
|
||||
:options="menuOptions"
|
||||
@update:value="handleMenuUpdate" />
|
||||
</n-layout-sider>
|
||||
|
||||
<n-layout-content class="content">
|
||||
<div class="content-container">
|
||||
<!-- 发现页视图 -->
|
||||
<v-list nav>
|
||||
<v-list-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:active="activeKey === item.value"
|
||||
@click="handleMenuClick(item.value)"
|
||||
rounded="xl"
|
||||
color="primary"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
<v-badge
|
||||
v-if="item.badge"
|
||||
:content="item.badge"
|
||||
color="error"
|
||||
inline
|
||||
class="ml-2"
|
||||
></v-badge>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<!-- Main Content -->
|
||||
<v-main>
|
||||
<v-container fluid class="pa-4">
|
||||
<!-- Discover View -->
|
||||
<div v-show="activeKey === 'discover'">
|
||||
<n-space vertical size="large" v-if="peers.length > 0">
|
||||
<div class="peer-grid">
|
||||
<div v-for="peer in peers" :key="peer.id">
|
||||
<PeerCard
|
||||
:peer="peer"
|
||||
@transferStarted="activeKey = 'transfers'" />
|
||||
</div>
|
||||
<div v-if="peers.length > 0" class="peer-grid">
|
||||
<div v-for="peer in peers" :key="peer.id">
|
||||
<PeerCard
|
||||
:peer="peer"
|
||||
@transferStarted="activeKey = 'transfers'"
|
||||
/>
|
||||
</div>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<n-empty description="Scanning for peers...">
|
||||
<template #icon>
|
||||
<n-icon class="radar-icon">
|
||||
<FontAwesomeIcon :icon="faSatelliteDish" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
<div
|
||||
v-else
|
||||
class="empty-state d-flex flex-column justify-center align-center"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-radar"
|
||||
size="100"
|
||||
color="primary"
|
||||
class="mb-4 radar-icon"
|
||||
style="opacity: 0.5"
|
||||
></v-icon>
|
||||
<div class="text-grey">Scanning for peers...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 传输列表视图 -->
|
||||
<!-- Transfers View -->
|
||||
<div v-show="activeKey === 'transfers'">
|
||||
<div v-if="transferList.length > 0">
|
||||
<TransferItem
|
||||
v-for="transfer in transferList"
|
||||
:key="transfer.id"
|
||||
:transfer="transfer" />
|
||||
:transfer="transfer"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<n-empty style="user-select: none" description="No transfers yet">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faInbox" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
<div
|
||||
v-else
|
||||
class="empty-state d-flex flex-column justify-center align-center"
|
||||
>
|
||||
<v-icon icon="mdi-inbox" size="100" class="mb-4 text-grey"></v-icon>
|
||||
<div class="text-grey">No transfers yet</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-header {
|
||||
height: 64px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.desktop-logo {
|
||||
margin-bottom: 24px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 90vh;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.radar-icon {
|
||||
animation: spin 3s linear infinite;
|
||||
color: #38bdf8;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@@ -271,7 +209,7 @@ const handleMenuUpdate = (key: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
@media (min-width: 960px) {
|
||||
.peer-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -1,40 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, h } from "vue";
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NIcon,
|
||||
NTag,
|
||||
NSpace,
|
||||
NDropdown,
|
||||
NSelect,
|
||||
type DropdownOption,
|
||||
NModal,
|
||||
NList,
|
||||
NListItem,
|
||||
NThing,
|
||||
NEmpty,
|
||||
NInput,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faLinux,
|
||||
faWindows,
|
||||
faApple,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faDesktop,
|
||||
faGlobe,
|
||||
faPaperPlane,
|
||||
faChevronDown,
|
||||
faFile,
|
||||
faFolder,
|
||||
faFont,
|
||||
faClipboard,
|
||||
faTrash,
|
||||
faPlus,
|
||||
faCloudArrowUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
|
||||
import { Dialogs, Events, Clipboard } from "@wailsio/runtime";
|
||||
import {
|
||||
@@ -72,52 +37,39 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const ipOptions = computed(() => {
|
||||
return ips.value.map((ip) => ({
|
||||
label: ip,
|
||||
value: ip,
|
||||
}));
|
||||
});
|
||||
|
||||
const osIcon = computed(() => {
|
||||
switch (props.peer.os) {
|
||||
case "linux":
|
||||
return faLinux;
|
||||
return "mdi-linux";
|
||||
case "windows":
|
||||
return faWindows;
|
||||
return "mdi-microsoft-windows";
|
||||
case "darwin":
|
||||
return faApple;
|
||||
return "mdi-apple";
|
||||
default:
|
||||
return faDesktop;
|
||||
return "mdi-desktop-classic";
|
||||
}
|
||||
});
|
||||
|
||||
const sendOptions: DropdownOption[] = [
|
||||
const sendOptions = [
|
||||
{
|
||||
label: "Send Files",
|
||||
key: "files",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFile }) }),
|
||||
title: "Send Files",
|
||||
value: "files",
|
||||
icon: "mdi-file",
|
||||
},
|
||||
{
|
||||
label: "Send Folder",
|
||||
key: "folder",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFolder }) }),
|
||||
title: "Send Folder",
|
||||
value: "folder",
|
||||
icon: "mdi-folder",
|
||||
},
|
||||
{
|
||||
label: "Send Text",
|
||||
key: "text",
|
||||
icon: () =>
|
||||
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFont }) }),
|
||||
title: "Send Text",
|
||||
value: "text",
|
||||
icon: "mdi-format-font",
|
||||
},
|
||||
{
|
||||
label: "Send Clipboard",
|
||||
key: "clipboard",
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(FontAwesomeIcon, { icon: faClipboard }),
|
||||
}),
|
||||
title: "Send Clipboard",
|
||||
value: "clipboard",
|
||||
icon: "mdi-clipboard",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -255,159 +207,180 @@ const handleSendFiles = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card hoverable class="peer-card">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<n-icon size="24">
|
||||
<FontAwesomeIcon :icon="osIcon" />
|
||||
</n-icon>
|
||||
<span style="user-select: none">{{ peer.name }}</span>
|
||||
<v-card hover link class="peer-card pa-2">
|
||||
<template v-slot:title>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">{{ peer.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-space vertical>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faGlobe" />
|
||||
</n-icon>
|
||||
<!-- Single IP Display -->
|
||||
<n-tag
|
||||
v-if="ips.length === 1"
|
||||
:bordered="false"
|
||||
type="info"
|
||||
size="small">
|
||||
{{ ips[0] }}
|
||||
</n-tag>
|
||||
<!-- Multiple IP Selector -->
|
||||
<n-select
|
||||
v-else-if="ips.length > 1"
|
||||
v-model:value="selectedIp"
|
||||
:options="ipOptions"
|
||||
size="small"
|
||||
style="width: 140px" />
|
||||
<!-- No Route -->
|
||||
<n-tag v-else :bordered="false" type="warning" size="small">
|
||||
No Route
|
||||
</n-tag>
|
||||
</div>
|
||||
</n-space>
|
||||
<template v-slot:text>
|
||||
<div class="d-flex align-center flex-wrap ga-2 mt-2">
|
||||
<v-icon icon="mdi-web" size="20" class="text-medium-emphasis"></v-icon>
|
||||
|
||||
<template #action>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="sendOptions"
|
||||
@select="handleAction"
|
||||
:disabled="ips.length === 0">
|
||||
<n-button type="primary" block dashed style="width: 100%">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faPaperPlane" />
|
||||
</n-icon>
|
||||
<!-- Single IP Display -->
|
||||
<v-chip v-if="ips.length === 1" size="small" color="info" label>
|
||||
{{ ips[0] }}
|
||||
</v-chip>
|
||||
|
||||
<!-- Multiple IP Selector -->
|
||||
<div v-else-if="ips.length > 1" style="width: 150px">
|
||||
<v-select
|
||||
v-model="selectedIp"
|
||||
:items="ips"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
single-line
|
||||
></v-select>
|
||||
</div>
|
||||
|
||||
<!-- No Route -->
|
||||
<v-chip v-else color="warning" size="small" label> No Route </v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
block
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
:disabled="ips.length === 0"
|
||||
append-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-send"></v-icon>
|
||||
</template>
|
||||
Send...
|
||||
<n-icon style="margin-left: 4px">
|
||||
<FontAwesomeIcon :icon="faChevronDown" />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, index) in sendOptions"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
@click="handleAction(item.value)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="item.icon"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</n-card>
|
||||
</v-card>
|
||||
|
||||
<!-- 文件发送 Modal -->
|
||||
<n-modal
|
||||
:mask-closable="false"
|
||||
v-model:show="showFileModal"
|
||||
preset="card"
|
||||
title="Send Files"
|
||||
style="width: 600px; max-width: 90%"
|
||||
:bordered="false">
|
||||
<div
|
||||
v-if="fileList.length === 0"
|
||||
class="drop-zone"
|
||||
@click="openFileDialog"
|
||||
data-file-drop-target>
|
||||
<n-empty description="Click to select files">
|
||||
<template #icon>
|
||||
<n-icon :size="48">
|
||||
<FontAwesomeIcon :icon="faCloudArrowUp" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
</div>
|
||||
<v-dialog v-model="showFileModal" width="600" persistent eager>
|
||||
<v-card title="Send Files">
|
||||
<v-card-text>
|
||||
<div
|
||||
v-if="fileList.length === 0"
|
||||
class="drop-zone pa-10 text-center rounded-lg border-dashed"
|
||||
@click="openFileDialog"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-cloud-upload"
|
||||
size="48"
|
||||
color="primary"
|
||||
class="mb-2"
|
||||
></v-icon>
|
||||
<div class="text-body-1 text-medium-emphasis">
|
||||
Click to select files
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div
|
||||
style="max-height: 400px; overflow-y: auto; margin-bottom: 16px"
|
||||
data-file-drop-target>
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="(file, index) in fileList" :key="file.path">
|
||||
<template #suffix>
|
||||
<n-button text type="error" @click="handleRemoveFile(index)">
|
||||
<template #icon>
|
||||
<n-icon><FontAwesomeIcon :icon="faTrash" /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-thing :title="file.name" :description="file.path"></n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
<n-button dashed block @click="openFileDialog">
|
||||
<template #icon>
|
||||
<n-icon><FontAwesomeIcon :icon="faPlus" /></n-icon>
|
||||
</template>
|
||||
Add more files
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-list
|
||||
class="mb-4 text-left"
|
||||
border
|
||||
rounded
|
||||
max-height="400"
|
||||
style="overflow-y: auto"
|
||||
data-file-drop-target
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(file, index) in fileList"
|
||||
:key="file.path"
|
||||
:title="file.name"
|
||||
:subtitle="file.path"
|
||||
lines="two"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click="handleRemoveFile(index)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="handleCancelFiles">Cancel</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
<v-btn
|
||||
block
|
||||
variant="outlined"
|
||||
style="border-style: dashed"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openFileDialog"
|
||||
class="mt-2"
|
||||
>
|
||||
Add more files
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="handleCancelFiles">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="handleSendFiles"
|
||||
:disabled="fileList.length === 0">
|
||||
:disabled="fileList.length === 0"
|
||||
>
|
||||
Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 文本发送 Modal -->
|
||||
<n-modal
|
||||
:mask-closable="false"
|
||||
v-model:show="showTextModal"
|
||||
preset="card"
|
||||
title="Send Text"
|
||||
style="width: 500px; max-width: 90%"
|
||||
:bordered="false">
|
||||
<n-input
|
||||
v-model:value="textContent"
|
||||
type="textarea"
|
||||
placeholder="Type something to send..."
|
||||
:autosize="{ minRows: 4, maxRows: 10 }" />
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showTextModal = false">Cancel</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
<v-dialog v-model="showTextModal" width="500" persistent eager>
|
||||
<v-card title="Send Text">
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
v-model="textContent"
|
||||
label="Content"
|
||||
placeholder="Type something to send..."
|
||||
rows="4"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="showTextModal = false">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="executeSendText"
|
||||
:disabled="!textContent">
|
||||
:disabled="!textContent"
|
||||
>
|
||||
Send
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drop-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
border: 2px dashed #666; /* Use a darker color or theme var */
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from "vue";
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
NIcon,
|
||||
NProgress,
|
||||
NSpace,
|
||||
NText,
|
||||
NTag,
|
||||
useMessage,
|
||||
NInput,
|
||||
NDropdown,
|
||||
NButtonGroup,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faArrowUp,
|
||||
faArrowDown,
|
||||
faCircleExclamation,
|
||||
faUser,
|
||||
faFile,
|
||||
faFileLines,
|
||||
faFolder,
|
||||
faClock,
|
||||
faChevronDown,
|
||||
faEye,
|
||||
faCopy,
|
||||
faTrash,
|
||||
faXmark,
|
||||
faStop,
|
||||
faCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { computed, ref, h } from "vue";
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import {
|
||||
ResolvePendingRequest,
|
||||
@@ -40,8 +8,6 @@ import {
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
import { useDialog } from "naive-ui";
|
||||
|
||||
const props = defineProps<{
|
||||
transfer: Transfer;
|
||||
}>();
|
||||
@@ -72,10 +38,10 @@ const percentage = computed(() =>
|
||||
),
|
||||
),
|
||||
);
|
||||
const progressStatus = computed(() => {
|
||||
const progressColor = computed(() => {
|
||||
if (props.transfer.status === "error") return "error";
|
||||
if (props.transfer.status === "completed") return "success";
|
||||
return "default";
|
||||
return "primary";
|
||||
});
|
||||
|
||||
const acceptTransfer = () => {
|
||||
@@ -99,10 +65,10 @@ const acceptToFolder = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownOptions = [
|
||||
const dropdownItems = [
|
||||
{
|
||||
label: "Accept To Folder",
|
||||
key: "folder",
|
||||
title: "Accept To Folder",
|
||||
value: "folder",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -116,31 +82,18 @@ const handleDelete = () => {
|
||||
DeleteTransfer(props.transfer.id);
|
||||
};
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const handleCopy = async () => {
|
||||
Clipboard.SetText(props.transfer.text)
|
||||
.then(() => {
|
||||
message.success("Copied to clipboard");
|
||||
})
|
||||
// .then(() => {
|
||||
// message.success("Copied to clipboard");
|
||||
// })
|
||||
.catch(() => {
|
||||
message.error("Failed to copy to clipboard");
|
||||
// message.error("Failed to copy to clipboard");
|
||||
console.error("Failed to copy");
|
||||
});
|
||||
};
|
||||
|
||||
const dialog = useDialog();
|
||||
const handleOpen = async () => {
|
||||
const d = dialog.create({
|
||||
title: "Text Content",
|
||||
content: () =>
|
||||
h(NInput, {
|
||||
value: props.transfer.text,
|
||||
readonly: true,
|
||||
type: "textarea",
|
||||
rows: 10,
|
||||
}),
|
||||
});
|
||||
};
|
||||
const showContentDialog = ref(false);
|
||||
|
||||
const canCancel = computed(() => {
|
||||
if (
|
||||
@@ -186,269 +139,212 @@ const canAccept = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card size="small" class="transfer-item">
|
||||
<div class="transfer-row">
|
||||
<!-- 图标 -->
|
||||
<div class="icon-wrapper">
|
||||
<n-icon size="24" v-if="props.transfer.type === 'send'" color="#38bdf8">
|
||||
<FontAwesomeIcon :icon="faArrowUp" />
|
||||
</n-icon>
|
||||
<n-icon
|
||||
size="24"
|
||||
v-else-if="props.transfer.type === 'receive'"
|
||||
color="#22c55e">
|
||||
<FontAwesomeIcon :icon="faArrowDown" />
|
||||
</n-icon>
|
||||
<n-icon size="24" v-else color="#f59e0b">
|
||||
<FontAwesomeIcon :icon="faCircleExclamation" />
|
||||
</n-icon>
|
||||
</div>
|
||||
|
||||
<!-- 信息 -->
|
||||
<div class="info-wrapper">
|
||||
<div class="header-line">
|
||||
<n-text
|
||||
v-if="props.transfer.content_type === 'file'"
|
||||
strong
|
||||
class="filename"
|
||||
:title="props.transfer.file_name">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faFile" />
|
||||
</n-icon>
|
||||
{{ props.transfer.file_name }}
|
||||
</n-text>
|
||||
<n-text
|
||||
v-else-if="props.transfer.content_type === 'text'"
|
||||
strong
|
||||
class="filename"
|
||||
title="Text">
|
||||
<n-icon> <FontAwesomeIcon :icon="faFileLines" /> </n-icon>
|
||||
Text</n-text
|
||||
>
|
||||
<n-text
|
||||
v-else-if="props.transfer.content_type === 'folder'"
|
||||
strong
|
||||
class="filename"
|
||||
title="Folder">
|
||||
<n-icon> <FontAwesomeIcon :icon="faFolder" /> </n-icon>
|
||||
{{ props.transfer.file_name || "Folder" }}</n-text
|
||||
>
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
v-if="
|
||||
props.transfer.sender.name && props.transfer.type === 'receive'
|
||||
">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faUser" />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ props.transfer.sender.name }}
|
||||
</n-tag>
|
||||
<n-tag
|
||||
size="small"
|
||||
:bordered="false"
|
||||
v-if="props.transfer.create_time">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faClock" />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ formatTime(props.transfer.create_time) }}
|
||||
</n-tag>
|
||||
<v-card class="transfer-item mb-2" variant="outlined">
|
||||
<v-card-text class="py-2 px-3">
|
||||
<div class="d-flex align-center flex-wrap ga-2">
|
||||
<!-- 图标 -->
|
||||
<div>
|
||||
<v-icon
|
||||
size="24"
|
||||
v-if="props.transfer.type === 'send'"
|
||||
color="info"
|
||||
icon="mdi-arrow-up"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
size="24"
|
||||
v-else-if="props.transfer.type === 'receive'"
|
||||
color="success"
|
||||
icon="mdi-arrow-down"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
size="24"
|
||||
v-else
|
||||
color="warning"
|
||||
icon="mdi-alert-circle"
|
||||
></v-icon>
|
||||
</div>
|
||||
|
||||
<div class="meta-line">
|
||||
<n-text depth="3" class="size">{{
|
||||
formatSize(props.transfer.file_size)
|
||||
}}</n-text>
|
||||
<!-- 信息 -->
|
||||
<div class="info-wrapper flex-grow-1" style="min-width: 0">
|
||||
<div class="d-flex align-center ga-2 mb-1 flex-wrap">
|
||||
<div class="font-weight-bold text-truncate d-flex align-center">
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
v-if="props.transfer.content_type === 'file'"
|
||||
icon="mdi-file"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
v-else-if="props.transfer.content_type === 'text'"
|
||||
icon="mdi-file-document"
|
||||
></v-icon>
|
||||
<v-icon
|
||||
size="small"
|
||||
class="mr-1"
|
||||
v-else-if="props.transfer.content_type === 'folder'"
|
||||
icon="mdi-folder"
|
||||
></v-icon>
|
||||
{{
|
||||
props.transfer.file_name ||
|
||||
(props.transfer.content_type === "text" ? "Text" : "Folder")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<!-- 状态文本(进行中/已完成) -->
|
||||
<span>
|
||||
<n-text depth="3" v-if="props.transfer.status === 'active'">
|
||||
- {{ formatSpeed(props.transfer.progress.speed) }}</n-text
|
||||
<v-chip
|
||||
size="x-small"
|
||||
v-if="
|
||||
props.transfer.sender.name && props.transfer.type === 'receive'
|
||||
"
|
||||
prepend-icon="mdi-account"
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
{{ props.transfer.sender.name }}
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
size="x-small"
|
||||
v-if="props.transfer.create_time"
|
||||
prepend-icon="mdi-clock-outline"
|
||||
>
|
||||
{{ formatTime(props.transfer.create_time) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-medium-emphasis d-flex align-center">
|
||||
<span>{{ formatSize(props.transfer.file_size) }}</span>
|
||||
|
||||
<!-- 状态文本 -->
|
||||
<span v-if="props.transfer.status === 'active'">
|
||||
- {{ formatSpeed(props.transfer.progress.speed) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="props.transfer.status === 'completed'"
|
||||
type="success">
|
||||
- Completed</n-text
|
||||
class="text-success"
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'error'"
|
||||
type="error">
|
||||
- {{ props.transfer.error_msg || "Error" }}</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'canceled'"
|
||||
type="info">
|
||||
- Canceled</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
- Completed
|
||||
</span>
|
||||
<span v-if="props.transfer.status === 'error'" class="text-error">
|
||||
- {{ props.transfer.error_msg || "Error" }}
|
||||
</span>
|
||||
<span v-if="props.transfer.status === 'canceled'" class="text-info">
|
||||
- Canceled
|
||||
</span>
|
||||
<span
|
||||
v-if="props.transfer.status === 'rejected'"
|
||||
type="error">
|
||||
- Rejected</n-text
|
||||
class="text-error"
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
- Rejected
|
||||
</span>
|
||||
<span
|
||||
v-if="props.transfer.status === 'pending'"
|
||||
type="warning">
|
||||
- Waiting for accept</n-text
|
||||
class="text-warning"
|
||||
>
|
||||
</span>
|
||||
- Waiting for accept
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<v-progress-linear
|
||||
v-if="props.transfer.status === 'active'"
|
||||
:model-value="percentage"
|
||||
:color="progressColor"
|
||||
height="4"
|
||||
striped
|
||||
class="mt-1"
|
||||
></v-progress-linear>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<n-progress
|
||||
v-if="props.transfer.status === 'active'"
|
||||
type="line"
|
||||
:percentage="percentage"
|
||||
:status="progressStatus"
|
||||
:height="4"
|
||||
:show-indicator="false"
|
||||
processing
|
||||
style="margin-top: 4px" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-wrapper">
|
||||
<n-space>
|
||||
<n-button-group size="small">
|
||||
<n-button v-if="canAccept" type="success" @click="acceptTransfer">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faCheck" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="dropdownOptions"
|
||||
@select="handleSelect"
|
||||
v-if="canAccept && props.transfer.content_type !== 'text'">
|
||||
<n-button type="success">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faChevronDown" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<n-button
|
||||
<!-- 操作按钮 -->
|
||||
<div class="actions-wrapper">
|
||||
<v-btn-group density="compact" variant="outlined" divided>
|
||||
<v-btn
|
||||
v-if="canAccept"
|
||||
size="small"
|
||||
type="error"
|
||||
@click="rejectTransfer">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faXmark" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
color="success"
|
||||
icon="mdi-check"
|
||||
@click="acceptTransfer"
|
||||
></v-btn>
|
||||
|
||||
<n-button type="success" @click="handleOpen" v-if="canCopy"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faEye" />
|
||||
</n-icon>
|
||||
<v-menu v-if="canAccept && props.transfer.content_type !== 'text'">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
color="success"
|
||||
icon="mdi-chevron-down"
|
||||
v-bind="props"
|
||||
></v-btn>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button type="success" @click="handleCopy" v-if="canCopy"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faCopy" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
type="success"
|
||||
@click="handleDelete"
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(item, index) in dropdownItems"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
@click="handleSelect(item.value)"
|
||||
>
|
||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<v-btn
|
||||
v-if="canAccept"
|
||||
color="error"
|
||||
icon="mdi-close"
|
||||
@click="rejectTransfer"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCopy"
|
||||
color="success"
|
||||
icon="mdi-eye"
|
||||
@click="showContentDialog = true"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCopy"
|
||||
color="success"
|
||||
icon="mdi-content-copy"
|
||||
@click="handleCopy"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="
|
||||
props.transfer.status === 'completed' ||
|
||||
props.transfer.status === 'error' ||
|
||||
props.transfer.status === 'canceled' ||
|
||||
props.transfer.status === 'rejected'
|
||||
">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faTrash" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
"
|
||||
color="info"
|
||||
icon="mdi-delete"
|
||||
@click="handleDelete"
|
||||
></v-btn>
|
||||
|
||||
<v-btn
|
||||
v-if="canCancel"
|
||||
size="small"
|
||||
type="error"
|
||||
color="error"
|
||||
icon="mdi-stop"
|
||||
@click="CancelTransfer(props.transfer.id)"
|
||||
><template #icon>
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faStop" />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
</n-space>
|
||||
></v-btn>
|
||||
</v-btn-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="showContentDialog" width="600">
|
||||
<v-card title="Text Content">
|
||||
<v-card-text>
|
||||
<v-textarea
|
||||
:model-value="props.transfer.text"
|
||||
readonly
|
||||
rows="10"
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.transfer-item {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.transfer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.actions-wrapper {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.transfer-row {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
// Styles
|
||||
import 'unfonts.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
15
frontend/src/plugins/index.ts
Normal file
15
frontend/src/plugins/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* plugins/index.ts
|
||||
*
|
||||
* Automatically included in `./src/main.ts`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
|
||||
// Types
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function registerPlugins (app: App) {
|
||||
app.use(vuetify)
|
||||
}
|
||||
19
frontend/src/plugins/vuetify.ts
Normal file
19
frontend/src/plugins/vuetify.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* plugins/vuetify.ts
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: {
|
||||
defaultTheme: 'system',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user