feat: hr indicator, adjust color theme
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
10
src/components/icons/TablerActivityHeartbeat.vue
Normal file
10
src/components/icons/TablerActivityHeartbeat.vue
Normal 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>
|
||||
10
src/components/icons/TablerCaretDownFilled.vue
Normal file
10
src/components/icons/TablerCaretDownFilled.vue
Normal 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>
|
||||
10
src/components/icons/TablerCaretUpFilled.vue
Normal file
10
src/components/icons/TablerCaretUpFilled.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user