refactor: replace naive-ui with vuetify

This commit is contained in:
2026-02-04 22:41:22 +08:00
parent a4173c327d
commit f7a881358f
26 changed files with 2853 additions and 1379 deletions

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go"
}
]
}

4
frontend/.browserslistrc Normal file
View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

6
frontend/.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

22
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,93 +0,0 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,18 +1,81 @@
# Vue 3 + TypeScript + Vite # Vuetify (Default)
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
## Recommended IDE Setup ## ❗️ Important Links
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). - 📄 [Docs](https://vuetifyjs.com/)
- 🚨 [Issues](https://issues.vuetifyjs.com/)
- 🏬 [Store](https://store.vuetifyjs.com/)
- 🎮 [Playground](https://play.vuetifyjs.com/)
- 💬 [Discord](https://community.vuetifyjs.com)
## Type Support For `.vue` Imports in TS ## 💿 Install
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: | Package Manager | Command |
|---------------------------------------------------------------|----------------|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
| [bun](https://bun.sh/#getting-started) | `bun install` |
1. Disable the built-in TypeScript Extension After completing the installation, your environment is ready for Vuetify development.
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` ## ✨ Features
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts-next for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts-next](https://github.com/loicduong/vite-plugin-vue-layouts-next)
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
-**Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
## 💡 Usage
This section covers how to start the development server and build your project for production.
### Starting the Development Server
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
```bash
yarn dev
```
(Repeat for npm, pnpm, and bun with respective commands.)
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
### Building for Production
To build your project for production, use:
```bash
yarn build
```
(Repeat for npm, pnpm, and bun with respective commands.)
Once the build process is completed, your application will be ready for deployment in a production environment.
## 💪 Support Vuetify Development
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
## 📑 License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present Vuetify, LLC

View File

@@ -0,0 +1,24 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
export function GetSavePath(): $CancellablePromise<string> {
return $Call.ByID(4081533263);
}
/**
* Save 保存配置到磁盘
*/
export function Save(): $CancellablePromise<void> {
return $Call.ByID(3089450934);
}
/**
* SetSavePath 修改配置
*/
export function SetSavePath(savePath: string): $CancellablePromise<void> {
return $Call.ByID(3805718491, savePath);
}

View File

@@ -0,0 +1,7 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as Config from "./config.js";
export {
Config
};

18
frontend/components.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
MainLayout: typeof import('./src/components/MainLayout.vue')['default']
PeerCard: typeof import('./src/components/PeerCard.vue')['default']
TransferItem: typeof import('./src/components/TransferItem.vue')['default']
}
}

1
frontend/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MeshDrop</title> <title>Mesh Drop</title>
</head> </head>
<body> <body>

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,35 @@
{ {
"name": "frontend", "name": "mesh-drop",
"private": true, "private": true,
"version": "0.0.0",
"type": "module", "type": "module",
"version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build:dev": "vue-tsc && vite build --minify false --mode development", "build:dev": "vite build",
"build": "vue-tsc && vite build --mode production", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview" "preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.1.0", "@fontsource/roboto": "5.2.7",
"@fortawesome/free-brands-svg-icons": "^7.1.0", "@mdi/font": "7.4.47",
"@fortawesome/free-regular-svg-icons": "^7.1.0", "vue": "^3.5.21",
"@fortawesome/free-solid-svg-icons": "^7.1.0", "vuetify": "^3.10.1",
"@fortawesome/vue-fontawesome": "^3.1.3", "@wailsio/runtime": "^3.0.0-alpha.79"
"@wailsio/runtime": "^3.0.0-alpha.79",
"naive-ui": "^2.43.2",
"vfonts": "^0.0.3",
"vue": "^3.2.45"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^4.0.0", "@tsconfig/node22": "^22.0.0",
"typescript": "^4.9.3", "@types/node": "^22.9.0",
"vite": "^5.0.0", "@vitejs/plugin-vue": "^6.0.1",
"vue-tsc": "^1.0.11" "@vue/tsconfig": "^0.8.1",
"npm-run-all2": "^8.0.4",
"sass-embedded": "^1.92.1",
"typescript": "~5.9.2",
"unplugin-fonts": "^1.4.0",
"unplugin-vue-components": "^29.0.0",
"vite": "^7.1.5",
"vite-plugin-vuetify": "^2.1.2",
"vue-tsc": "^3.2.0"
} }
} }

View File

@@ -1,39 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import {
NConfigProvider,
NGlobalStyle,
NMessageProvider,
NDialogProvider,
darkTheme,
} from "naive-ui";
import MainLayout from "./components/MainLayout.vue"; import MainLayout from "./components/MainLayout.vue";
const themeOverrides = {
common: {
primaryColor: "#38bdf8",
primaryColorHover: "#0ea5e9",
},
Card: {
borderColor: "#334155",
},
};
</script> </script>
<template> <template>
<n-config-provider :theme="darkTheme" :theme-overrides="themeOverrides"> <v-app theme="dark">
<n-global-style />
<n-dialog-provider>
<n-message-provider>
<MainLayout /> <MainLayout />
</n-message-provider> </v-app>
</n-dialog-provider>
</n-config-provider>
</template> </template>
<style> <style>
body, body,
#app, #app {
.n-config-provider {
font-family: "Noto Sans", "Roboto", "Segoe UI", sans-serif !important; font-family: "Noto Sans", "Roboto", "Segoe UI", sans-serif !important;
} }
</style> </style>

View File

@@ -1,28 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, computed, h } from "vue"; import { onMounted, ref, computed } from "vue";
import PeerCard from "./PeerCard.vue"; import PeerCard from "./PeerCard.vue";
import TransferItem from "./TransferItem.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 { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
import { Transfer } from "../../bindings/mesh-drop/internal/transfer"; import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
import { GetPeers } from "../../bindings/mesh-drop/internal/discovery/service"; 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 peers = ref<Peer[]>([]);
const transferList = ref<Transfer[]>([]); const transferList = ref<Transfer[]>([]);
const activeKey = ref("discover"); const activeKey = ref("discover");
const showMobileMenu = ref(false); const drawer = ref(true); // Control drawer visibility
const isMobile = ref(false); const isMobile = ref(false);
// 监听窗口大小变化更新 isMobile // 监听窗口大小变化更新 isMobile
@@ -43,49 +22,20 @@ onMounted(async () => {
transferList.value = ( transferList.value = (
(list || []).filter((t) => t !== null) as Transfer[] (list || []).filter((t) => t !== null) as Transfer[]
).sort((a, b) => b.create_time - a.create_time); ).sort((a, b) => b.create_time - a.create_time);
if (isMobile.value) {
drawer.value = false;
}
}); });
const checkMobile = () => { const checkMobile = () => {
isMobile.value = window.innerWidth < 768; const mobile = window.innerWidth < 768;
if (!isMobile.value) showMobileMenu.value = false; 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 () => { onMounted(async () => {
peers.value = await GetPeers(); peers.value = await GetPeers();
@@ -111,146 +61,134 @@ const pendingCount = computed(() => {
).length; ).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; activeKey.value = key;
showMobileMenu.value = false; if (isMobile.value) {
drawer.value = false;
}
}; };
</script> </script>
<template> <template>
<!-- 小尺寸头部 --> <v-layout>
<n-layout-header v-if="isMobile" bordered class="mobile-header"> <!-- App Bar for Mobile -->
<n-space <v-app-bar v-if="isMobile" border flat>
align="center" <v-toolbar-title class="text-primary font-weight-bold"
justify="space-between" >Mesh Drop</v-toolbar-title
style="height: 100%; padding: 0 16px"> >
<n-text class="logo">Mesh Drop</n-text> <template v-slot:append>
<n-button <v-btn icon="mdi-menu" @click="drawer = !drawer"></v-btn>
text </template>
style="font-size: 24px" </v-app-bar>
@click="showMobileMenu = !showMobileMenu">
<n-icon>
<FontAwesomeIcon :icon="showMobileMenu ? faXmark : faBars" />
</n-icon>
</n-button>
</n-space>
</n-layout-header>
<!-- 小尺寸抽屉菜单 --> <!-- Navigation Drawer -->
<n-drawer <v-navigation-drawer v-model="drawer" :permanent="!isMobile">
v-model:show="showMobileMenu" <div class="pa-4" v-if="!isMobile">
placement="top" <div class="text-h6 text-primary font-weight-bold">Mesh Drop</div>
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>
</div> </div>
<n-menu
:value="activeKey"
:options="menuOptions"
@update:value="handleMenuUpdate" />
</n-layout-sider>
<n-layout-content class="content"> <v-list nav>
<div class="content-container"> <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'"> <div v-show="activeKey === 'discover'">
<n-space vertical size="large" v-if="peers.length > 0"> <div v-if="peers.length > 0" class="peer-grid">
<div class="peer-grid">
<div v-for="peer in peers" :key="peer.id"> <div v-for="peer in peers" :key="peer.id">
<PeerCard <PeerCard
:peer="peer" :peer="peer"
@transferStarted="activeKey = 'transfers'" /> @transferStarted="activeKey = 'transfers'"
</div> />
</div>
</n-space>
<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> </div>
</div> </div>
<!-- 传输列表视图 --> <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-show="activeKey === 'transfers'">
<div v-if="transferList.length > 0"> <div v-if="transferList.length > 0">
<TransferItem <TransferItem
v-for="transfer in transferList" v-for="transfer in transferList"
:key="transfer.id" :key="transfer.id"
:transfer="transfer" /> :transfer="transfer"
/>
</div> </div>
<div v-else class="empty-state"> <div
<n-empty style="user-select: none" description="No transfers yet"> v-else
<template #icon> class="empty-state d-flex flex-column justify-center align-center"
<n-icon> >
<FontAwesomeIcon :icon="faInbox" /> <v-icon icon="mdi-inbox" size="100" class="mb-4 text-grey"></v-icon>
</n-icon> <div class="text-grey">No transfers yet</div>
</template>
</n-empty>
</div> </div>
</div> </div>
</div> </v-container>
</n-layout-content> </v-main>
</n-layout> </v-layout>
</template> </template>
<style scoped> <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 { .empty-state {
display: flex; height: 80vh;
justify-content: center;
align-items: center;
height: 90vh;
} }
.radar-icon { .radar-icon {
animation: spin 3s linear infinite; animation: spin 3s linear infinite;
color: #38bdf8;
opacity: 0.5;
} }
@keyframes spin { @keyframes spin {
@@ -271,7 +209,7 @@ const handleMenuUpdate = (key: string) => {
} }
} }
@media (min-width: 700px) { @media (min-width: 960px) {
.peer-grid { .peer-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }

View File

@@ -1,40 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch, h } from "vue"; import { computed, ref, watch } 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 { Peer } from "../../bindings/mesh-drop/internal/discovery/models"; import { Peer } from "../../bindings/mesh-drop/internal/discovery/models";
import { Dialogs, Events, Clipboard } from "@wailsio/runtime"; import { Dialogs, Events, Clipboard } from "@wailsio/runtime";
import { import {
@@ -72,52 +37,39 @@ watch(
{ immediate: true }, { immediate: true },
); );
const ipOptions = computed(() => {
return ips.value.map((ip) => ({
label: ip,
value: ip,
}));
});
const osIcon = computed(() => { const osIcon = computed(() => {
switch (props.peer.os) { switch (props.peer.os) {
case "linux": case "linux":
return faLinux; return "mdi-linux";
case "windows": case "windows":
return faWindows; return "mdi-microsoft-windows";
case "darwin": case "darwin":
return faApple; return "mdi-apple";
default: default:
return faDesktop; return "mdi-desktop-classic";
} }
}); });
const sendOptions: DropdownOption[] = [ const sendOptions = [
{ {
label: "Send Files", title: "Send Files",
key: "files", value: "files",
icon: () => icon: "mdi-file",
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFile }) }),
}, },
{ {
label: "Send Folder", title: "Send Folder",
key: "folder", value: "folder",
icon: () => icon: "mdi-folder",
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFolder }) }),
}, },
{ {
label: "Send Text", title: "Send Text",
key: "text", value: "text",
icon: () => icon: "mdi-format-font",
h(NIcon, null, { default: () => h(FontAwesomeIcon, { icon: faFont }) }),
}, },
{ {
label: "Send Clipboard", title: "Send Clipboard",
key: "clipboard", value: "clipboard",
icon: () => icon: "mdi-clipboard",
h(NIcon, null, {
default: () => h(FontAwesomeIcon, { icon: faClipboard }),
}),
}, },
]; ];
@@ -255,159 +207,180 @@ const handleSendFiles = () => {
</script> </script>
<template> <template>
<n-card hoverable class="peer-card"> <v-card hover link class="peer-card pa-2">
<template #header> <template v-slot:title>
<div style="display: flex; align-items: center; gap: 8px"> <div class="d-flex align-center">
<n-icon size="24"> <v-icon :icon="osIcon" size="24" class="mr-2"></v-icon>
<FontAwesomeIcon :icon="osIcon" /> <span class="text-subtitle-1 font-weight-bold">{{ peer.name }}</span>
</n-icon>
<span style="user-select: none">{{ peer.name }}</span>
</div> </div>
</template> </template>
<n-space vertical> <template v-slot:text>
<div style="display: flex; align-items: center; gap: 8px"> <div class="d-flex align-center flex-wrap ga-2 mt-2">
<n-icon> <v-icon icon="mdi-web" size="20" class="text-medium-emphasis"></v-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 #action> <!-- Single IP Display -->
<div style="display: flex; gap: 8px"> <v-chip v-if="ips.length === 1" size="small" color="info" label>
<n-dropdown {{ ips[0] }}
trigger="click" </v-chip>
:options="sendOptions"
@select="handleAction" <!-- Multiple IP Selector -->
:disabled="ips.length === 0"> <div v-else-if="ips.length > 1" style="width: 150px">
<n-button type="primary" block dashed style="width: 100%"> <v-select
<template #icon> v-model="selectedIp"
<n-icon> :items="ips"
<FontAwesomeIcon :icon="faPaperPlane" /> density="compact"
</n-icon> 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> </template>
Send... Send...
<n-icon style="margin-left: 4px"> </v-btn>
<FontAwesomeIcon :icon="faChevronDown" />
</n-icon>
</n-button>
</n-dropdown>
</div>
</template> </template>
</n-card> <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>
</v-card>
<!-- 文件发送 Modal --> <!-- 文件发送 Modal -->
<n-modal <v-dialog v-model="showFileModal" width="600" persistent eager>
:mask-closable="false" <v-card title="Send Files">
v-model:show="showFileModal" <v-card-text>
preset="card"
title="Send Files"
style="width: 600px; max-width: 90%"
:bordered="false">
<div <div
v-if="fileList.length === 0" v-if="fileList.length === 0"
class="drop-zone" class="drop-zone pa-10 text-center rounded-lg border-dashed"
@click="openFileDialog" @click="openFileDialog"
data-file-drop-target> data-file-drop-target
<n-empty description="Click to select files"> >
<template #icon> <v-icon
<n-icon :size="48"> icon="mdi-cloud-upload"
<FontAwesomeIcon :icon="faCloudArrowUp" /> size="48"
</n-icon> color="primary"
</template> class="mb-2"
</n-empty> ></v-icon>
<div class="text-body-1 text-medium-emphasis">
Click to select files
</div>
</div> </div>
<div v-else> <div v-else>
<div <v-list
style="max-height: 400px; overflow-y: auto; margin-bottom: 16px" class="mb-4 text-left"
data-file-drop-target> border
<n-list bordered> rounded
<n-list-item v-for="(file, index) in fileList" :key="file.path"> max-height="400"
<template #suffix> style="overflow-y: auto"
<n-button text type="error" @click="handleRemoveFile(index)"> data-file-drop-target
<template #icon> >
<n-icon><FontAwesomeIcon :icon="faTrash" /></n-icon> <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> </template>
</n-button> </v-list-item>
</template> </v-list>
<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>
<template #footer> <v-btn
<n-space justify="end"> block
<n-button @click="handleCancelFiles">Cancel</n-button> variant="outlined"
<n-button style="border-style: dashed"
type="primary" 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" @click="handleSendFiles"
:disabled="fileList.length === 0"> :disabled="fileList.length === 0"
>
Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }} Send {{ fileList.length > 0 ? `(${fileList.length})` : "" }}
</n-button> </v-btn>
</n-space> </v-card-actions>
</template> </v-card>
</n-modal> </v-dialog>
<!-- 文本发送 Modal --> <!-- 文本发送 Modal -->
<n-modal <v-dialog v-model="showTextModal" width="500" persistent eager>
:mask-closable="false" <v-card title="Send Text">
v-model:show="showTextModal" <v-card-text>
preset="card" <v-textarea
title="Send Text" v-model="textContent"
style="width: 500px; max-width: 90%" label="Content"
:bordered="false">
<n-input
v-model:value="textContent"
type="textarea"
placeholder="Type something to send..." placeholder="Type something to send..."
:autosize="{ minRows: 4, maxRows: 10 }" /> rows="4"
<template #footer> auto-grow
<n-space justify="end"> ></v-textarea>
<n-button @click="showTextModal = false">Cancel</n-button> </v-card-text>
<n-button <v-card-actions>
type="primary" <v-spacer></v-spacer>
<v-btn variant="text" @click="showTextModal = false">Cancel</v-btn>
<v-btn
color="primary"
@click="executeSendText" @click="executeSendText"
:disabled="!textContent"> :disabled="!textContent"
>
Send Send
</n-button> </v-btn>
</n-space> </v-card-actions>
</template> </v-card>
</n-modal> </v-dialog>
</template> </template>
<style scoped> <style scoped>
.drop-zone { .drop-zone {
border: 2px dashed #ccc; border: 2px dashed #666; /* Use a darker color or theme var */
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
} }

View File

@@ -1,37 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, h } from "vue"; import { computed, ref, 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 { Transfer } from "../../bindings/mesh-drop/internal/transfer"; import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
import { import {
ResolvePendingRequest, ResolvePendingRequest,
@@ -40,8 +8,6 @@ import {
} from "../../bindings/mesh-drop/internal/transfer/service"; } from "../../bindings/mesh-drop/internal/transfer/service";
import { Dialogs, Clipboard } from "@wailsio/runtime"; import { Dialogs, Clipboard } from "@wailsio/runtime";
import { useDialog } from "naive-ui";
const props = defineProps<{ const props = defineProps<{
transfer: Transfer; 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 === "error") return "error";
if (props.transfer.status === "completed") return "success"; if (props.transfer.status === "completed") return "success";
return "default"; return "primary";
}); });
const acceptTransfer = () => { const acceptTransfer = () => {
@@ -99,10 +65,10 @@ const acceptToFolder = async () => {
} }
}; };
const dropdownOptions = [ const dropdownItems = [
{ {
label: "Accept To Folder", title: "Accept To Folder",
key: "folder", value: "folder",
}, },
]; ];
@@ -116,31 +82,18 @@ const handleDelete = () => {
DeleteTransfer(props.transfer.id); DeleteTransfer(props.transfer.id);
}; };
const message = useMessage();
const handleCopy = async () => { const handleCopy = async () => {
Clipboard.SetText(props.transfer.text) Clipboard.SetText(props.transfer.text)
.then(() => { // .then(() => {
message.success("Copied to clipboard"); // message.success("Copied to clipboard");
}) // })
.catch(() => { .catch(() => {
message.error("Failed to copy to clipboard"); // message.error("Failed to copy to clipboard");
console.error("Failed to copy");
}); });
}; };
const dialog = useDialog(); const showContentDialog = ref(false);
const handleOpen = async () => {
const d = dialog.create({
title: "Text Content",
content: () =>
h(NInput, {
value: props.transfer.text,
readonly: true,
type: "textarea",
rows: 10,
}),
});
};
const canCancel = computed(() => { const canCancel = computed(() => {
if ( if (
@@ -186,269 +139,212 @@ const canAccept = computed(() => {
</script> </script>
<template> <template>
<n-card size="small" class="transfer-item"> <v-card class="transfer-item mb-2" variant="outlined">
<div class="transfer-row"> <v-card-text class="py-2 px-3">
<div class="d-flex align-center flex-wrap ga-2">
<!-- 图标 --> <!-- 图标 -->
<div class="icon-wrapper"> <div>
<n-icon size="24" v-if="props.transfer.type === 'send'" color="#38bdf8"> <v-icon
<FontAwesomeIcon :icon="faArrowUp" /> size="24"
</n-icon> v-if="props.transfer.type === 'send'"
<n-icon color="info"
icon="mdi-arrow-up"
></v-icon>
<v-icon
size="24" size="24"
v-else-if="props.transfer.type === 'receive'" v-else-if="props.transfer.type === 'receive'"
color="#22c55e"> color="success"
<FontAwesomeIcon :icon="faArrowDown" /> icon="mdi-arrow-down"
</n-icon> ></v-icon>
<n-icon size="24" v-else color="#f59e0b"> <v-icon
<FontAwesomeIcon :icon="faCircleExclamation" /> size="24"
</n-icon> v-else
color="warning"
icon="mdi-alert-circle"
></v-icon>
</div> </div>
<!-- 信息 --> <!-- 信息 -->
<div class="info-wrapper"> <div class="info-wrapper flex-grow-1" style="min-width: 0">
<div class="header-line"> <div class="d-flex align-center ga-2 mb-1 flex-wrap">
<n-text <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'" v-if="props.transfer.content_type === 'file'"
strong icon="mdi-file"
class="filename" ></v-icon>
:title="props.transfer.file_name"> <v-icon
<n-icon> size="small"
<FontAwesomeIcon :icon="faFile" /> class="mr-1"
</n-icon>
{{ props.transfer.file_name }}
</n-text>
<n-text
v-else-if="props.transfer.content_type === 'text'" v-else-if="props.transfer.content_type === 'text'"
strong icon="mdi-file-document"
class="filename" ></v-icon>
title="Text"> <v-icon
<n-icon> <FontAwesomeIcon :icon="faFileLines" /> </n-icon> size="small"
Text</n-text class="mr-1"
>
<n-text
v-else-if="props.transfer.content_type === 'folder'" v-else-if="props.transfer.content_type === 'folder'"
strong icon="mdi-folder"
class="filename" ></v-icon>
title="Folder"> {{
<n-icon> <FontAwesomeIcon :icon="faFolder" /> </n-icon> props.transfer.file_name ||
{{ props.transfer.file_name || "Folder" }}</n-text (props.transfer.content_type === "text" ? "Text" : "Folder")
> }}
<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>
</div> </div>
<div class="meta-line"> <v-chip
<n-text depth="3" class="size">{{ size="x-small"
formatSize(props.transfer.file_size) v-if="
}}</n-text> props.transfer.sender.name && props.transfer.type === 'receive'
"
prepend-icon="mdi-account"
>
{{ props.transfer.sender.name }}
</v-chip>
<!-- 状态文本(进行中/已完成) --> <v-chip
<span> size="x-small"
<n-text depth="3" v-if="props.transfer.status === 'active'"> v-if="props.transfer.create_time"
&nbsp;- {{ formatSpeed(props.transfer.progress.speed) }}</n-text prepend-icon="mdi-clock-outline"
> >
<n-text {{ formatTime(props.transfer.create_time) }}
depth="3" </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'">
&nbsp;- {{ formatSpeed(props.transfer.progress.speed) }}
</span>
<span
v-if="props.transfer.status === 'completed'" v-if="props.transfer.status === 'completed'"
type="success"> class="text-success"
&nbsp;- Completed</n-text
> >
<n-text &nbsp;- Completed
depth="3" </span>
v-if="props.transfer.status === 'error'" <span v-if="props.transfer.status === 'error'" class="text-error">
type="error"> &nbsp;- {{ props.transfer.error_msg || "Error" }}
&nbsp;- {{ props.transfer.error_msg || "Error" }}</n-text </span>
> <span v-if="props.transfer.status === 'canceled'" class="text-info">
<n-text &nbsp;- Canceled
depth="3" </span>
v-if="props.transfer.status === 'canceled'" <span
type="info">
&nbsp;- Canceled</n-text
>
<n-text
depth="3"
v-if="props.transfer.status === 'rejected'" v-if="props.transfer.status === 'rejected'"
type="error"> class="text-error"
&nbsp;- Rejected</n-text
> >
<n-text &nbsp;- Rejected
depth="3" </span>
<span
v-if="props.transfer.status === 'pending'" v-if="props.transfer.status === 'pending'"
type="warning"> class="text-warning"
&nbsp;- Waiting for accept</n-text
> >
&nbsp;- Waiting for accept
</span> </span>
</div> </div>
<!-- 进度条 --> <!-- 进度条 -->
<n-progress <v-progress-linear
v-if="props.transfer.status === 'active'" v-if="props.transfer.status === 'active'"
type="line" :model-value="percentage"
:percentage="percentage" :color="progressColor"
:status="progressStatus" height="4"
:height="4" striped
:show-indicator="false" class="mt-1"
processing ></v-progress-linear>
style="margin-top: 4px" />
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="actions-wrapper"> <div class="actions-wrapper">
<n-space> <v-btn-group density="compact" variant="outlined" divided>
<n-button-group size="small"> <v-btn
<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
v-if="canAccept" v-if="canAccept"
size="small" color="success"
type="error" icon="mdi-check"
@click="rejectTransfer"> @click="acceptTransfer"
<template #icon> ></v-btn>
<n-icon>
<FontAwesomeIcon :icon="faXmark" />
</n-icon>
</template>
</n-button>
<n-button type="success" @click="handleOpen" v-if="canCopy" <v-menu v-if="canAccept && props.transfer.content_type !== 'text'">
><template #icon> <template v-slot:activator="{ props }">
<n-icon> <v-btn
<FontAwesomeIcon :icon="faEye" /> color="success"
</n-icon> icon="mdi-chevron-down"
v-bind="props"
></v-btn>
</template> </template>
</n-button> <v-list>
<n-button type="success" @click="handleCopy" v-if="canCopy" <v-list-item
><template #icon> v-for="(item, index) in dropdownItems"
<n-icon> :key="index"
<FontAwesomeIcon :icon="faCopy" /> :value="item.value"
</n-icon> @click="handleSelect(item.value)"
</template> >
</n-button> <v-list-item-title>{{ item.title }}</v-list-item-title>
<n-button </v-list-item>
type="success" </v-list>
@click="handleDelete" </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=" v-if="
props.transfer.status === 'completed' || props.transfer.status === 'completed' ||
props.transfer.status === 'error' || props.transfer.status === 'error' ||
props.transfer.status === 'canceled' || props.transfer.status === 'canceled' ||
props.transfer.status === 'rejected' props.transfer.status === 'rejected'
"> "
<template #icon> color="info"
<n-icon> icon="mdi-delete"
<FontAwesomeIcon :icon="faTrash" /> @click="handleDelete"
</n-icon> ></v-btn>
</template>
</n-button> <v-btn
<n-button
v-if="canCancel" v-if="canCancel"
size="small" color="error"
type="error" icon="mdi-stop"
@click="CancelTransfer(props.transfer.id)" @click="CancelTransfer(props.transfer.id)"
><template #icon> ></v-btn>
<n-icon> </v-btn-group>
<FontAwesomeIcon :icon="faStop" />
</n-icon>
</template>
</n-button>
</n-button-group>
</n-space>
</div> </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> </template>
<style scoped> <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 { .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; 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> </style>

View File

@@ -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' 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')

View 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)
}

View 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',
},
})

View File

@@ -11,9 +11,18 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"lib": ["ESNext", "DOM"], "lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true "noEmit": true
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "bindings"], "include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"bindings"
],
} }

55
frontend/vite.config.mts Normal file
View File

@@ -0,0 +1,55 @@
// Plugins
import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import Fonts from 'unplugin-fonts/vite'
import wails from "@wailsio/runtime/plugins/vite";
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
Vue({
template: { transformAssetUrls },
}),
wails("./bindings"),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify(),
Components(),
Fonts({
fontsource: {
families: [
{
name: 'Roboto',
weights: [100, 300, 400, 500, 700, 900],
styles: ['normal', 'italic'],
},
],
},
}),
],
optimizeDeps: {
exclude: ['vuetify'],
},
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})

View File

@@ -1,8 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import wails from "@wailsio/runtime/plugins/vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), wails("./bindings")],
});

2
go.mod
View File

@@ -5,6 +5,7 @@ go 1.25
require ( require (
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/spf13/viper v1.21.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.67 github.com/wailsapp/wails/v3 v3.0.0-alpha.67
) )
@@ -64,7 +65,6 @@ require (
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect

2
go.sum
View File

@@ -34,6 +34,8 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=

View File

@@ -61,6 +61,9 @@ func main() {
X: conf.WindowState.X, X: conf.WindowState.X,
Y: conf.WindowState.Y, Y: conf.WindowState.Y,
EnableFileDrop: true, EnableFileDrop: true,
Linux: application.LinuxWindow{
WebviewGpuPolicy: application.WebviewGpuPolicyAlways,
},
}) })
window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) { window.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {