feat: base frame

This commit is contained in:
2024-07-16 21:14:07 +08:00
parent 71dc75a092
commit 1d213eae57
33 changed files with 1493 additions and 123 deletions

1
components.d.ts vendored
View File

@@ -11,5 +11,6 @@ declare module 'vue' {
Greet: typeof import('./src/components/Greet.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TablerDeviceWatch: typeof import('./src/components/icons/TablerDeviceWatch.vue')['default']
}
}

View File

@@ -17,7 +17,12 @@
"devDependencies": {
"@tauri-apps/cli": "^1",
"@vitejs/plugin-vue": "^5.0.5",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"sass": "^1.77.8",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",
"unplugin-vue-components": "^0.27.2",
"vite": "^5.3.1",
"vue-tsc": "^2.0.22"
}

1023
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

11
src-tauri/Cargo.lock generated
View File

@@ -2670,6 +2670,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -3216,7 +3225,9 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",

View File

@@ -15,7 +15,7 @@ tauri = { version = "1", features = [ "window-show", "window-hide", "window-maxi
serde = { version = "1", features = ["derive"] }
serde_json = "1"
btleplug = { version = "0.11.5", features = ["serde"] }
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread", "macros"] }
tokio = { version = "1.38.0", features = ["full"] }
futures = "0.3.30"
uuid = "1.10.0"
lazy_static = "1.5.0"

View File

@@ -1,132 +1,219 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use tauri::{AppHandle, Manager, State};
use tokio;
use tokio::sync::{Mutex};
use btleplug::api::{Central, ScanFilter, Manager as _, Peripheral as _};
use btleplug::api::bleuuid::uuid_from_u16;
use btleplug::api::CentralEvent::{
DeviceConnected, DeviceDisconnected, DeviceDiscovered, DeviceUpdated,
ManufacturerDataAdvertisement, ServiceDataAdvertisement, ServicesAdvertisement,
};
use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter};
use btleplug::platform::{Adapter, Manager as BtleManager, Peripheral};
use futures::StreamExt;
use tauri::{AppHandle, Manager, State};
use tokio;
use tokio::sync::Mutex;
#[derive(serde::Serialize, Clone)]
struct BleDevice {
name: String,
address: String,
name: String,
address: String,
}
struct BleConnection {
manager: Mutex<Option<BtleManager>>,
adapter: Mutex<Option<Adapter>>,
peripheral: Mutex<Option<Peripheral>>,
struct BleConnection<'a> {
central: Mutex<Option<Adapter>>,
peripheral: &'a Mutex<Option<Peripheral>>,
scan_devices: Mutex<Vec<BleDevice>>,
}
#[tauri::command]
async fn scan_devices(
connection: State<'_, BleConnection>
) -> Result<String, String> {
let adapter = connection.adapter.lock().await;
let adapter = adapter.as_ref().unwrap();
adapter.start_scan(ScanFilter::default()).await.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
async fn request_central_events<'a>(
app_handle: AppHandle,
connection: State<'a, BleConnection<'a>>,
) -> Result<bool, String> {
let central = connection.central.lock().await;
let peripheral = connection.peripheral.lock().await;
let central = central.as_ref().unwrap();
let central = central.clone();
let mut devices = vec![];
for p in adapter.peripherals().await.unwrap() {
devices.push(BleDevice {
name: p.properties().await.unwrap().unwrap().local_name.unwrap_or("Unknown".to_string()),
address: p.address().to_string(),
let mut event_stream = central.events().await.unwrap();
tauri::async_runtime::spawn(async move {
while let Some(event) = event_stream.next().await {
if let DeviceDiscovered(_) | DeviceUpdated(_) = event.clone() {
let mut devices = vec![];
for p in central.peripherals().await.unwrap() {
devices.push(BleDevice {
name: p
.properties()
.await
.unwrap()
.unwrap()
.local_name
.unwrap_or("Unknown".to_string()),
address: p.address().to_string(),
});
}
println!("Devices: {:?}", serde_json::to_string(&devices).unwrap());
app_handle
.emit_all("scan-list-update", serde_json::to_string(&devices).unwrap())
.unwrap();
}
if let DeviceConnected(_) = event.clone() {
let peripheral = peripheral.as_ref().unwrap().clone();
app_handle
.emit_all(
"device-connected",
serde_json::to_string(&BleDevice {
name: peripheral
.properties()
.await
.unwrap()
.unwrap()
.local_name
.unwrap_or("Unknown".to_string()),
address: peripheral.address().to_string(),
})
.unwrap(),
)
.unwrap();
}
}
});
}
Ok(serde_json::to_string(&devices).unwrap())
Ok(true)
}
#[tauri::command]
async fn start_scan(connection: State<'_, BleConnection>) -> Result<bool, String> {
let central = connection.central.lock().await;
let central = central.as_ref().unwrap();
central
.start_scan(ScanFilter::default())
.await
.unwrap_or_else(|_| {
println!("Failed to start scan");
});
Ok(true)
}
#[tauri::command]
async fn stop_scan(connection: State<'_, BleConnection>) -> Result<bool, String> {
let central = connection.central.lock().await;
let central = central.as_ref().unwrap();
central.stop_scan().await.unwrap_or_else(|_| {
println!("Failed to stop scan");
});
Ok(true)
}
#[tauri::command]
async fn connect(
address: String,
connection: State<'_, BleConnection>,
app_handle: AppHandle,
address: String,
connection: State<'_, BleConnection>,
app_handle: AppHandle,
) -> Result<bool, String> {
let adapter = connection.adapter.lock().await;
let adapter = adapter.as_ref().unwrap();
// adapter.start_scan(ScanFilter::default()).await.unwrap();
// tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let central = connection.central.lock().await;
let central = central.as_ref().unwrap();
let peripheral = adapter.peripherals().await.unwrap().into_iter().find(|p| p.address().to_string() == address).unwrap().clone();
peripheral.connect().await.unwrap();
let peripheral = central
.peripherals()
.await
.unwrap()
.into_iter()
.find(|p| p.address().to_string() == address)
.unwrap()
.clone();
peripheral.connect().await.unwrap();
*connection.peripheral.lock().await = Some(peripheral.clone());
*connection.peripheral.lock().await = Some(peripheral.clone());
peripheral.discover_services().await.unwrap_or_else(|_| {
println!("Failed to discover services");
});
let service = peripheral.services()
.into_iter()
.find(|s| s.uuid == uuid_from_u16(0x180D))
.unwrap();
let characteristic = service.characteristics
.into_iter()
.find(|c| c.uuid == uuid_from_u16(0x2A37))
.unwrap();
peripheral.discover_services().await.unwrap_or_else(|_| {
println!("Failed to discover services");
});
let service = peripheral
.services()
.into_iter()
.find(|s| s.uuid == uuid_from_u16(0x180D))
.unwrap();
let characteristic = service
.characteristics
.into_iter()
.find(|c| c.uuid == uuid_from_u16(0x2A37))
.unwrap();
peripheral.subscribe(&characteristic).await.unwrap_or_else(|_| {
println!("Failed to subscribe to characteristic");
});
peripheral
.subscribe(&characteristic)
.await
.unwrap_or_else(|_| {
println!("Failed to subscribe to characteristic");
});
tokio::spawn(async move {
let mut notification_stream = peripheral.notifications().await.unwrap();
while let Some(notification) = notification_stream.next().await {
if notification.uuid == uuid_from_u16(0x2A37) {
app_handle.emit_all("heart-rate", notification.value[1]).unwrap();
}
}
});
tokio::spawn(async move {
let mut notification_stream = peripheral.notifications().await.unwrap();
while let Some(notification) = notification_stream.next().await {
if notification.uuid == uuid_from_u16(0x2A37) {
app_handle
.emit_all("heart-rate", notification.value[1])
.unwrap();
}
}
});
Ok(true)
Ok(true)
}
#[tauri::command]
async fn disconnect(
connection: State<'_, BleConnection>
) -> Result<bool, String> {
let connection = connection.peripheral.lock().await;
let connection = connection.as_ref().unwrap();
async fn disconnect(connection: State<'_, BleConnection>) -> Result<bool, String> {
let connection = connection.peripheral.lock().await;
let connection = connection.as_ref().unwrap();
connection.disconnect().await.unwrap();
connection.disconnect().await.unwrap();
Ok(true)
Ok(true)
}
#[tauri::command]
async fn get_connected_device(
connection: State<'_, BleConnection>
) -> Result<String, String> {
let connection = connection.peripheral.lock().await;
let connection = connection.as_ref().unwrap();
async fn get_connected_device(connection: State<'_, BleConnection>) -> Result<String, String> {
let connection = connection.peripheral.lock().await;
let connection = connection.as_ref().unwrap();
Ok(serde_json::to_string(&BleDevice {
name: connection.properties().await.unwrap().unwrap().local_name.unwrap_or("Unknown".to_string()),
address: connection.address().to_string(),
}).unwrap())
Ok(serde_json::to_string(&BleDevice {
name: connection
.properties()
.await
.unwrap()
.unwrap()
.local_name
.unwrap_or("Unknown".to_string()),
address: connection.address().to_string(),
})
.unwrap())
}
#[tokio::main]
async fn main() {
let ble_manager = BtleManager::new().await.unwrap();
let adapter = ble_manager.adapters().await.unwrap().into_iter().next().unwrap();
let ble_manager = BtleManager::new().await.unwrap();
let central = ble_manager
.adapters()
.await
.unwrap()
.into_iter()
.next()
.unwrap();
tauri::Builder::default()
.manage(BleConnection {
manager: Mutex::new(Some(ble_manager)),
adapter: Mutex::new(Some(adapter)),
peripheral: Default::default(),
})
.invoke_handler(tauri::generate_handler![
scan_devices,
connect,
disconnect,
get_connected_device
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
tauri::Builder::default()
.manage(BleConnection {
central: Mutex::new(Some(central)),
peripheral: Default::default(),
scan_devices: Default::default(),
})
.invoke_handler(tauri::generate_handler![
request_central_events,
start_scan,
stop_scan,
connect,
disconnect,
get_connected_device
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -32,9 +32,9 @@
{
"title": "Heartbeat Cat",
"minWidth": 800,
"minHeight": 600,
"minHeight": 500,
"width": 800,
"height": 600
"height": 500
}
],
"security": {

View File

@@ -1,11 +1,22 @@
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { RouterView } from "vue-router";
import DrawerContainer from "./components/DrawerContainer.vue";
invoke("request_central_events").then(res => {
console.log('central event listening', res);
})
listen("heart-rate", (hr) => {
console.log('Heart Rate', hr);
})
listen("scan-list-update", (event) => {
console.log('scan-list-update', event.payload);
})
listen("device-connected", (event) => {
console.log('device-connected', event);
})
</script>
<template>
@@ -13,7 +24,7 @@ listen("heart-rate", (hr) => {
<DrawerContainer>
<RouterView v-slot="{ Component }">
<Transition name="scale" mode="out-in">
<KeepAlive>
<KeepAlive :exclude="['settings']">
<component :is="Component" />
</KeepAlive>
</Transition>
@@ -23,6 +34,9 @@ listen("heart-rate", (hr) => {
</template>
<style>
@import './assets/css/font.css';
@import './assets/css/style.css';
:root {
--app-background: #f8f8f8;
@@ -65,13 +79,14 @@ body {
}
body {
font-family: Rubik, 'Noto Sans SC', sans-serif;
font-weight: 400;
background-color: var(--app-background);
font-family: MiSans, sans-serif;
border-radius: 8px;
overflow: hidden;
}
.app-top-bar {
.titlebar {
-webkit-user-select: none;
user-select: none;
-webkit-app-region: drag;
@@ -90,7 +105,7 @@ body {
overflow: hidden;
}
.app-top-bar .icon {
.titlebar .icon {
height: calc(var(--title-bar-height) - 6px);
}
</style>

109
src/assets/css/font.css Normal file
View File

@@ -0,0 +1,109 @@
/* noto-sans-sc-200 - chinese-simplified_latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 200;
src: url('../fonts/noto-sans-sc-v36-chinese-simplified_latin-200.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-regular - chinese-simplified_latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 400;
src: url('../fonts/noto-sans-sc-v36-chinese-simplified_latin-regular.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-600 - chinese-simplified_latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 600;
src: url('../fonts/noto-sans-sc-v36-chinese-simplified_latin-600.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* noto-sans-sc-800 - chinese-simplified_latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Noto Sans SC';
font-style: normal;
font-weight: 800;
src: url('../fonts/noto-sans-sc-v36-chinese-simplified_latin-800.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-300 - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: normal;
font-weight: 300;
src: url('../fonts/rubik-v28-latin-300.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-300italic - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: italic;
font-weight: 300;
src: url('../fonts/rubik-v28-latin-300italic.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-regular - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: normal;
font-weight: 400;
src: url('../fonts/rubik-v28-latin-regular.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-italic - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: italic;
font-weight: 400;
src: url('../fonts/rubik-v28-latin-italic.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-600 - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: normal;
font-weight: 600;
src: url('../fonts/rubik-v28-latin-600.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}
/* rubik-600italic - latin */
@font-face {
font-display: swap;
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
font-family: 'Rubik';
font-style: italic;
font-weight: 600;
src: url('../fonts/rubik-v28-latin-600italic.woff2') format('woff2');
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
}

20
src/assets/css/style.css Normal file
View File

@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
-webkit-user-drag: none;
@apply select-none;
}
}
@layer utilities {
.allow-drag {
-webkit-user-drag: auto;
}
.allow-select {
@apply select-text;
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,9 +3,9 @@ import { ref } from 'vue'
const navList = ref([
{ title: '设备连接', path: '/' },
{ title: '心率曲线', path: '/hr-chart' },
{ title: '心率曲线', path: '/charts' },
{ title: '桌面组件', path: '/widgets' },
{ title: '推流插件', path: '/streaming-plugin' },
{ title: '推流插件', path: '/streaming-plugins' },
{ title: '设置', path: '/settings' },
])
</script>
@@ -14,25 +14,48 @@ const navList = ref([
<div class="drawer">
<div class="side">
<div class="nav">
<router-link
v-for="item in navList"
:key="item.path"
:to="item.path"
class="nav-item"
active-class="active"
>
{{ item.title }}
<router-link v-for="item in navList" :key="item.path" :to="item.path" class="nav-item"
active-class="active">
<span>{{ item.title }}</span>
</router-link>
</div>
<div class="status"></div>
<div class="status">
status
</div>
</div>
<div class="content">
<slot />
</div>
<slot />
</div>
</template>
<style scoped>
.drawer {
display: flex;
height: 100%;
background-color: var(--app-background);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23ccc' fill-opacity='0.1' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
@apply w-full h-screen flex;
.side {
background-color: var(--drawer-bar-background);
@apply w-[120px] flex flex-col;
.nav {
@apply flex-1 flex flex-col;
.nav-item {
background-color: var(--drawer-bar-item-background);
@apply flex justify-center items-center px-1 py-3.5 text-xs text-neutral-500 font-semibold transition-colors duration-150;
&.active {
/* background-color: var(--drawer-bar-item-active-background); */
@apply text-pink-400 border-l-4 border-pink-400 bg-pink-400/10 pl-0;
}
}
}
}
.content {
@apply flex-1;
}
}
</style>

View File

@@ -22,7 +22,7 @@ async function disconnect() {
}
async function scan() {
invoke("scan_devices").then(res => {
invoke("start_scan").then(res => {
console.log(JSON.parse(res as string));
})
}

View File

@@ -0,0 +1,10 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3zm3 9v3h6v-3M9 6V3h6v3"></path></svg>
</template>
<script>
export default {
name: 'TablerDeviceWatch'
}
</script>

View File

@@ -2,9 +2,14 @@ import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import Index from "./pages/index.vue";
const routes = [{ path: "/", component: Index }];
const routes = [
{ path: "/", component: () => import("./pages/index.vue") },
{ path: "/charts", component: () => import("./pages/charts.vue") },
{ path: "/widgets", component: () => import("./pages/widgets.vue") },
{ path: "/streaming-plugins", component: () => import("./pages/streaming-plugins.vue") },
{ path: "/settings", component: () => import("./pages/settings.vue") },
];
const router = createRouter({
history: createWebHistory(),

13
src/pages/charts.vue Normal file
View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<div>
Charts
</div>
</template>
<style scoped>
</style>

View File

@@ -3,7 +3,7 @@
<template>
<div>
index
<Greet />
</div>
</template>

12
src/pages/settings.vue Normal file
View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
</script>
<template>
<div>
Settings Page
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<div>
Streaming Plugins
</div>
</template>
<style scoped>
</style>

13
src/pages/widgets.vue Normal file
View File

@@ -0,0 +1,13 @@
<script lang="ts" setup>
</script>
<template>
<div>
Widgets Page
</div>
</template>
<style scoped>
</style>

8
tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -20,6 +20,6 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["components.d.ts", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,9 +1,15 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from "unplugin-vue-components/vite";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [vue()],
plugins: [
vue(),
Components({
dts: true,
}),
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//