feat: hr indicator, adjust color theme

This commit is contained in:
2024-07-17 20:39:34 +08:00
parent ad1125af3e
commit 577063d520
17 changed files with 185 additions and 57 deletions

3
components.d.ts vendored
View File

@@ -18,10 +18,13 @@ declare module 'vue' {
SystemUiconsSignalFull: typeof import('./src/components/icons/SystemUiconsSignalFull.vue')['default']
SystemUiconsSignalLow: typeof import('./src/components/icons/SystemUiconsSignalLow.vue')['default']
SystemUiconsSignalMedium: typeof import('./src/components/icons/SystemUiconsSignalMedium.vue')['default']
TablerActivityHeartbeat: typeof import('./src/components/icons/TablerActivityHeartbeat.vue')['default']
TablerBluetooth: typeof import('./src/components/icons/TablerBluetooth.vue')['default']
TablerBluetoothConnected: typeof import('./src/components/icons/TablerBluetoothConnected.vue')['default']
TablerBluetoothOff: typeof import('./src/components/icons/TablerBluetoothOff.vue')['default']
TablerBluetoothX: typeof import('./src/components/icons/TablerBluetoothX.vue')['default']
TablerCaretDownFilled: typeof import('./src/components/icons/TablerCaretDownFilled.vue')['default']
TablerCaretUpFilled: typeof import('./src/components/icons/TablerCaretUpFilled.vue')['default']
TablerDeviceWatch: typeof import('./src/components/icons/TablerDeviceWatch.vue')['default']
TablerReload: typeof import('./src/components/icons/TablerReload.vue')['default']
}

View File

@@ -2,10 +2,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use btleplug::api::bleuuid::uuid_from_u16;
use btleplug::api::CentralEvent::{
DeviceConnected, DeviceDisconnected, DeviceDiscovered, DeviceUpdated,
ManufacturerDataAdvertisement, ServiceDataAdvertisement, ServicesAdvertisement,
};
use btleplug::api::CentralEvent::{DeviceDisconnected, DeviceDiscovered, DeviceUpdated};
use btleplug::api::{BDAddr, Central, Manager as _, Peripheral as _, ScanFilter};
use btleplug::platform::{Adapter, Manager as BtleManager, Peripheral, PeripheralId};
use futures::StreamExt;
@@ -88,6 +85,7 @@ impl BleConnection {
{
return Err("Peripheral does not have the required service".into());
}
self.set_peripheral(Some(peripheral)).await;
let peripheral = self.peripheral.lock().await;
@@ -110,6 +108,40 @@ impl BleConnection {
.rssi
.unwrap(),
};
let service = peripheral
.as_ref()
.unwrap()
.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();
let peripheral = peripheral.clone();
peripheral
.as_ref()
.unwrap()
.subscribe(&characteristic)
.await?;
let app_clone = app.clone();
tokio::spawn(async move {
let mut notification_stream =
peripheral.as_ref().unwrap().notifications().await.unwrap();
while let Some(notification) = notification_stream.next().await {
if notification.uuid == uuid_from_u16(0x2A37) {
let value = notification.value;
let heart_rate = value[1] as u16;
app_clone.emit_all("heart-rate", heart_rate).unwrap();
}
}
});
app.emit_all("device-connected", device).unwrap();
Ok(())
}
@@ -127,7 +159,7 @@ impl BleConnection {
let app_handle = app.clone(); // Clone the AppHandle to move into the tokio::spawn closure
let mut event_stream = central.as_ref().unwrap().events().await.unwrap();
let mut self_clone = self.clone(); // Clone the BleConnection to move into the tokio::spawn closure
let self_clone = self.clone(); // Clone the BleConnection to move into the tokio::spawn closure
tokio::spawn(async move {
while let Some(event) = event_stream.next().await {

View File

@@ -6,8 +6,8 @@
"distDir": "../dist"
},
"package": {
"productName": "heartbeat-cat",
"version": "0.0.1"
"productName": "HBCat",
"version": "1.0.1"
},
"tauri": {
"allowlist": {
@@ -37,7 +37,8 @@
"minWidth": 800,
"minHeight": 500,
"width": 800,
"height": 500
"height": 500,
"userAgent": "Heartbeat Cat"
}
],
"security": {
@@ -47,9 +48,20 @@
"active": true,
"targets": "all",
"identifier": "ga.bh8.heartbeat-cat",
"publisher": "TimothyYin",
"copyright": "2024 TimothyYin",
"category": "Utility",
"shortDescription": "HBCat",
"longDescription": "Catch your heartbeat",
"icon": [
"icons/icon.ico"
]
],
"windows": {
"nsis": {
"installerIcon": "icons/icon.ico",
"installMode": "both"
}
}
}
}
}
}

View File

@@ -7,10 +7,6 @@ const store = useBrcatStore();
invoke("register_central_events");
listen("heart-rate", (hr) => {
console.log('Heart Rate', hr);
})
listen("scan-list-update", (event) => {
console.log('scan-list-update', event.payload);
})
@@ -52,7 +48,7 @@ listen("device-disconnected", (event) => {
--app-background: #f8f8f8;
--title-bar-background: #e4e4e4;
--title-bar-color: #F25E86;
--title-bar-color: var(--primary-color);
--title-bar-height: 25px;
--drawer-bar-background: #eeeeee;

View File

@@ -71,7 +71,6 @@
}
.btn {
border: none;
outline: none;
padding: 6px 12px;
min-width: 30px;
@@ -79,7 +78,6 @@
cursor: pointer;
text-transform: uppercase;
color: #fff;
background-color: #F25E86;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
@@ -90,22 +88,19 @@
-moz-transition: all .3s ease;
-ms-transition: all .3s ease;
-o-transition: all .3s ease;
@apply text-xs;
@apply text-xs bg-primary border-0;
}
.btn.outline {
background-color: transparent;
color: #F25E86;
border: 1px solid #F25E86;
box-shadow: none;
@apply text-primary bg-transparent border border-primary shadow-none;
}
.btn:hover {
box-shadow: 2px 2px 6px 1px rgb(0 0 0 / 20%);
box-shadow: 2px 2px 6px 1px rgb(0 0 0 / .1);
}
.btn:active {
box-shadow: 2px 2px 6px 1px rgb(0 0 0 / 20%), 2px 2px 6px 1px rgb(0 0 0 / 20%) inset;
box-shadow: 2px 2px 6px 1px rgb(0 0 0 / .1), 2px 2px 6px 1px rgb(0 0 0 / .1) inset;
}
.btn:disabled {

View File

@@ -21,12 +21,33 @@ const navList = ref([
<span>{{ item.title }}</span>
</router-link>
</div>
<div class="status">
<TablerBluetoothConnected v-if="store.is_connected" class="icon text-sm block -mt-0.5 text-emerald-500" />
<TablerBluetooth v-else class="icon text-sm block -mt-0.5 text-neutral-400" />
<span class="text" :title="store.is_connected ? store.connected_device?.name : undefined">
{{ store.is_connected ? store.connected_device?.name : '未连接' }}
</span>
<div>
<Transition name="indicator" mode="out-in">
<div v-if="store.is_connected"
class="flex px-2 py-0.5 gap-1 justify-between items-center bg-neutral-200 border-b border-neutral-300">
<span class="inline-flex items-center gap-0.5 text-2xl text-primary font-bold">
<TablerActivityHeartbeat class="text-base" />
{{ store.current_heart_rate || '--' }}
</span>
<span class="flex flex-col gap-0.5 text-neutral-400">
<span class="flex items-center gap-1 text-xs leading-none">
<TablerCaretUpFilled />
<span>{{ '--' }}</span>
</span>
<span class="flex items-center gap-1 text-xs leading-none">
<TablerCaretDownFilled />
<span>{{ '--' }}</span>
</span>
</span>
</div>
</Transition>
<div class="status">
<TablerBluetoothConnected v-if="store.is_connected" class="icon text-sm block -mt-0.5 text-emerald-500" />
<TablerBluetooth v-else class="icon text-sm block -mt-0.5 text-neutral-400" />
<span class="text" :title="store.is_connected ? store.connected_device?.name : undefined">
{{ store.is_connected ? store.connected_device?.name : '未连接' }}
</span>
</div>
</div>
</div>
<div class="content">
@@ -36,6 +57,16 @@ const navList = ref([
</template>
<style scoped>
.indicator-enter-active,
.indicator-leave-active {
@apply transition-all duration-150;
}
.indicator-enter-from,
.indicator-leave-to {
@apply opacity-0 translate-y-4;
}
.drawer {
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");
@@ -54,13 +85,13 @@ const navList = ref([
&.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;
@apply text-primary-400 border-l-4 border-primary-400 bg-primary-400/10 pl-0;
}
}
}
.status {
@apply w-full h-8 bg-neutral-300 flex items-center text-neutral-900 px-2 text-2xs whitespace-nowrap flex-nowrap;
@apply w-full h-8 bg-neutral-200 flex items-center text-neutral-900 px-2 text-2xs whitespace-nowrap flex-nowrap;
.text {
@apply flex-1 overflow-hidden text-ellipsis;

View File

@@ -15,7 +15,7 @@ defineProps({
<template>
<div class="flex flex-col h-full">
<header
class="w-full h-11 px-4 flex justify-between items-center bg-gradient-to-r from-pink-50 to-pink-100 border-b">
class="w-full h-11 px-4 flex justify-between items-center bg-gradient-to-r from-primary-50 to-primary-100 border-b">
<div class="flex flex-col gap-0.5">
<h1 class="font-semibold text-neutral-900" :class="{ 'text-xs': !!subtitle, 'text-base': !subtitle }">
{{ title }}

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="M3 12h4.5L9 6l4 12l2-9l1.5 3H21"></path></svg>
</template>
<script>
export default {
name: 'TablerActivityHeartbeat'
}
</script>

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="currentColor" d="M18 9c.852 0 1.297.986.783 1.623l-.076.084l-6 6a1 1 0 0 1-1.32.083l-.094-.083l-6-6l-.083-.094l-.054-.077l-.054-.096l-.017-.036l-.027-.067l-.032-.108l-.01-.053l-.01-.06l-.004-.057v-.118l.005-.058l.009-.06l.01-.052l.032-.108l.027-.067l.07-.132l.065-.09l.073-.081l.094-.083l.077-.054l.096-.054l.036-.017l.067-.027l.108-.032l.053-.01l.06-.01l.057-.004z"></path></svg>
</template>
<script>
export default {
name: 'TablerCaretDownFilled'
}
</script>

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="currentColor" d="M11.293 7.293a1 1 0 0 1 1.32-.083l.094.083l6 6l.083.094l.054.077l.054.096l.017.036l.027.067l.032.108l.01.053l.01.06l.004.057L19 14l-.002.059l-.005.058l-.009.06l-.01.052l-.032.108l-.027.067l-.07.132l-.065.09l-.073.081l-.094.083l-.077.054l-.096.054l-.036.017l-.067.027l-.108.032l-.053.01l-.06.01l-.057.004L18 15H6c-.852 0-1.297-.986-.783-1.623l.076-.084z"></path></svg>
</template>
<script>
export default {
name: 'TablerCaretUpFilled'
}
</script>

View File

@@ -3,11 +3,11 @@
</script>
<template>
<div>
Charts
</div>
<PageContainer title="心率记录">
<div class="w-full h-full flex justify-center items-center bg-white">
<span class="text-sm font-semibold text-neutral-400">前面的区域以后再来探索吧</span>
</div>
</PageContainer>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -57,8 +57,7 @@ onMounted(() => {
<SvgSpinnersPulse2 class="icon text-5xl text-neutral-400" />
<span class="text-sm font-semibold text-neutral-400">正在扫描蓝牙设备</span>
</div>
<div v-else-if="is_connecting"
class="w-full h-full flex flex-col gap-4 justify-center items-center">
<div v-else-if="is_connecting" class="w-full h-full flex flex-col gap-4 justify-center items-center">
<SvgSpinnersWifiFade class="icon text-5xl text-neutral-400" />
<span class="text-sm font-semibold text-neutral-400">正在连接到设备</span>
</div>

View File

@@ -2,11 +2,11 @@
</script>
<template>
<div>
Settings Page
</div>
<PageContainer title="设置">
<div class="w-full h-full flex justify-center items-center bg-white">
<span class="text-sm font-semibold text-neutral-400">前面的区域以后再来探索吧</span>
</div>
</PageContainer>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -3,9 +3,11 @@
</script>
<template>
<div>
Streaming Plugins
</div>
<PageContainer title="推流插件">
<div class="w-full h-full flex justify-center items-center bg-white">
<span class="text-sm font-semibold text-neutral-400">前面的区域以后再来探索吧</span>
</div>
</PageContainer>
</template>
<style scoped>

View File

@@ -3,11 +3,11 @@
</script>
<template>
<div>
Widgets Page
</div>
<PageContainer title="桌面组件">
<div class="w-full h-full flex justify-center items-center bg-white">
<span class="text-sm font-semibold text-neutral-400">前面的区域以后再来探索吧</span>
</div>
</PageContainer>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { ref, watchEffect } from "vue";
import { onMounted, ref, watchEffect } from "vue";
import { invoke } from "@tauri-apps/api/tauri";
import { listen } from "@tauri-apps/api/event";
import { useThrottleFn } from "@vueuse/core";
export interface Device {
@@ -17,6 +18,16 @@ export const useBrcatStore = defineStore("brcat", () => {
const is_scanning = ref(false);
const current_heart_rate = ref<Number>(0);
onMounted(async () => {
const unlisten = await listen("heart-rate", (heart_rate) => {
current_heart_rate.value = heart_rate.payload as Number;
});
return unlisten;
})
setInterval(async () => {
is_connected.value = await invoke("is_connected");
}, 500);
@@ -64,6 +75,7 @@ export const useBrcatStore = defineStore("brcat", () => {
}
return {
current_heart_rate,
is_connected,
is_scanning,
connected_device,

View File

@@ -4,6 +4,22 @@ export default <Config>{
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
primary: {
"50": "#fef1f6",
"100": "#fee5ef",
"200": "#ffcbe1",
"300": "#ffa1c6",
"400": "#ff6ea3",
"500": "#fa3a7b",
"600": "#ea1854",
"700": "#cc0a3c",
"800": "#a80c32",
"900": "#8c0f2d",
"950": "#560116",
DEFAULT: "#ff6ea3",
},
},
fontSize: {
"2xs": [
"0.625rem",