diff --git a/src/components/battery/README.md b/src/components/battery/README.md index e69de29..563b7b2 100644 --- a/src/components/battery/README.md +++ b/src/components/battery/README.md @@ -0,0 +1,43 @@ +# 电池功能组件 + +## 描述 + +显示电池卡片 UI + +## 接口 + +## 大小规格 + +提供 2x1, 1x2, 1x1 大小规格 + +## 示意图 + +``` +┌───────────────────────────────┐ +│ │ +├───────────────────────────────┤ +│ ┌───────bg──────┐ │ +│ │ ┌─text───┐ │ ┌─icon────┐ │ +│ │ │ 100 % │ │ │ thunder │ │ +│ │ └────────┘ │ └─────────┘ │ +│ └───────────────┘ │ +└───────────────────────────────┘ +┌───────────────────────────────┐ +│ │ +├───────────────────────────────┤ +│ ┌─────circle──────┐ │ +│ │ ┌─icon────┐ │ │ +│ │ │ thunder │ │ │ +│ │ └─────────┘ │ │ +│ │ ┌─text────┐ │ │ +│ │ │ 100 % │ │ │ +│ │ └─────────┘ │ │ +│ └─────────────────┘ │ +└───────────────────────────────┘ +┌───────────────────────────┐ +│ │ +│ ┌─text───┐ ┌─icon────┐ │ +│ │ 100 % │ │ thunder │ │ +│ └────────┘ └─────────┘ │ +└───────────────────────────┘ +``` diff --git a/src/components/battery/base.ts b/src/components/battery/base.ts new file mode 100644 index 0000000..efb4599 --- /dev/null +++ b/src/components/battery/base.ts @@ -0,0 +1,35 @@ +export type BatteryThemeType = 'dm' | 'lm' + +export type BatteryMode = 'power-normal' | 'power-save' + +export enum BatteryHealth { + 'Good', + 'Overheat', + 'Cold', + 'Warm', + 'Cool', + 'Unknown', +} + +export type BatteryEventType = + | 'chargingchange' + | 'chargingtimechange' + | 'dischargingtimechange' + | 'levelchange' + | 'batteryhealthchange' + +export interface BatteryManager extends EventTarget { + readonly charging: boolean + readonly chargingTime: number + readonly dischargingTime: number + readonly level: number + readonly temperature: number + readonly health: BatteryHealth + readonly present: boolean + + onchargingchange: (ev: Event) => any + onchargingtimechange: (ev: Event) => any + ondischargingtimechange: (ev: Event) => any + onlevelchange: (ev: Event) => any + onbatteryhealthchange: (ev: Event) => any +} diff --git a/src/components/battery/dynamic-icon/README.md b/src/components/battery/dynamic-icon/README.md new file mode 100644 index 0000000..1655975 --- /dev/null +++ b/src/components/battery/dynamic-icon/README.md @@ -0,0 +1,46 @@ +# 电池图标 + +## 描述 + +可用于状态栏、指示器等地方 + +## 电池图标显示模式 + +1. 常规模式 + 1. `(常规)(充电图标)(绿色)` + 2. `(常规)(百分比)(灰色)` + 3. `(常规)(百分比)(红色)` + 4. `(常规)(灰色)` + 5. `(常规)(红色)` +2. 省电模式 + 1. `(省电)(充电图标)(黄色)` + 2. `(省电)(百分比)(黄色)` + 3. `(省电)(黄色)` + +## 显示分支流程 + +1. 是否在充电 + 1. 是,是什么模式 + 1. 常规模式,`(常规)(充电图标)(绿色)` + 2. 省电模式,`(省电)(充电图标)(黄色)` + 2. 否,是否有百分比 + 1. 有,是什么模式 + 1. 常规模式,电量是否大于等于 20% + 1. 是,`(常规)(百分比)(灰色)` + 2. 否,`(常规)(百分比)(红色)` + 2. 省电模式,`(省电)(百分比)(黄色)` + 2. 否,是什么模式 + 1. 常规模式,电量是否大于等于 20% + 1. 是,`(常规)(灰色)` + 2. 否,`(常规)(红色)` + 2. 省电模式,`(省电)(黄色)` + +## 接口属性 + +| 字段 | 类型 | 值 | 优先级 | 备注 | +| ---------- | ------- | ----------------------- | ------ | --------------------------------------------------------- | +| charge | boolean | false/true | 1 | 指示充电 | +| theme | string | light/dark | 1 | 指示浅色/深色模式,控制电池边框、充电图标、百分比数字颜色 | +| percentage | boolean | false/true | 2 | 指示充电指示百分比 | +| percents | number | [0,100] | 2 | 指示充电指示百分比 | +| mode | string | power-normal/power-save | 3 | 指示模式,和图标颜色关联 | diff --git a/src/components/battery/dynamic-icon/icon.ts b/src/components/battery/dynamic-icon/icon.ts new file mode 100644 index 0000000..af73dbf --- /dev/null +++ b/src/components/battery/dynamic-icon/icon.ts @@ -0,0 +1,167 @@ +import { + css, + customElement, + html, + ifDefined, + property, + state, + LitElement, +} from '@star-web-components/base' +import {BatteryMode, BatteryThemeType} from '../base' + +@customElement('battery-dynamic-icon') +export class BatteryDynamicIcon extends LitElement { + @property({type: Boolean}) charge!: boolean + + @property({type: Boolean}) percentage!: boolean + + @property({type: String}) theme!: BatteryThemeType + + /** + * 百分比数字区间: [0, 100] + */ + @property({type: Number}) percents = 100 + + @property({type: String}) mode: BatteryMode = 'power-normal' + + @state() lowpower = false + + attributeChangedCallback(name: string, _old: string, value: string) { + super.attributeChangedCallback(name, _old, value) + if (name === 'percents') { + const _value = Number(value) + if (_value > 100 || _value < 0) { + throw new Error('Set wrong number on battery-icons percents') + } else { + this.lowpower = _value < 20 + } + this.style.setProperty('--battery-percents', String(this.percents)) + } + } + + render() { + return html` + + + + + + + ${this.percents} + + + + + + + + + + + + ` + } + + static styles = css` + svg { + width: 100%; + height: 100%; + font-size: 12px; + --green: #02b56d; + --yellow: #f5c125; + --red: #ec4949; + --brown: rgba(51, 51, 51, 0.3); + --border-color: #333333; + --percents-color: #262626; + --battery-background: var(--brown); + --rect-width: var(--battery-width, 24); + } + + svg[theme='dm'] { + --border-color: #f4f4f4; + --percents-color: #f0f0f0; + --brown: rgba(244, 244, 244, 0.3); + } + + svg[mode='power-normal'] { + --battery-background: var(--brown); + } + + svg[mode='power-normal'][charge] { + --battery-background: var(--green); + } + + svg[mode='power-normal'][lowpower]:not([charge]) { + --battery-background: var(--red); + } + + svg[mode='power-save'] { + --battery-background: var(--yellow); + } + + rect#rect-width { + width: calc(var(--battery-percents, 100) * 24px / 100); + } + + path#thunder-icon, + text#percents-text { + display: none; + } + + svg[charge] path#thunder-icon, + svg[percentage]:not([charge]) text#percents-text { + display: block; + } + + @media (prefers-color-scheme: dark) { + svg { + --border-color: #f4f4f4; + --percents-color: #f0f0f0; + --brown: rgba(244, 244, 244, 0.3); + } + } + ` +} + +declare global { + interface HTMLElementTagNameMap { + 'battery-dynamic-icon': BatteryDynamicIcon + } +} diff --git a/src/components/battery/index.ts b/src/components/battery/index.ts new file mode 100644 index 0000000..a1c93b2 --- /dev/null +++ b/src/components/battery/index.ts @@ -0,0 +1,2 @@ +export * from './rect-card/rect-card.js' +export * from './dynamic-icon/icon.js' diff --git a/src/components/battery/package.json b/src/components/battery/package.json new file mode 100644 index 0000000..1119874 --- /dev/null +++ b/src/components/battery/package.json @@ -0,0 +1,28 @@ +{ + "name": "@star-web-components/battery", + "version": "0.0.1", + "description": "", + "type": "module", + "main": "./index.js", + "module": "./index.js", + "exports": { + ".": { + "default": "./index.js" + }, + "./index": { + "default": "./index.js" + }, + "./package.json": "./package.json", + "./base": { + "default": "./base.js" + }, + "./rect-card": { + "default": "./rect-card/rect-card.js" + }, + "./dynamic-icon/icon.js": { + "default": "./dynamic-icon/icon.js" + } + }, + "author": "", + "license": "ISC" +} diff --git a/src/components/battery/rect-card/rect-card.ts b/src/components/battery/rect-card/rect-card.ts new file mode 100644 index 0000000..f91ec33 --- /dev/null +++ b/src/components/battery/rect-card/rect-card.ts @@ -0,0 +1,287 @@ +import { + css, + customElement, + html, + ifDefined, + property, + state, + LitElement, +} from '@star-web-components/base' +import {BatteryThemeType} from '../base' + +@customElement('battery-rect-card') +export class BatteryRectCard extends LitElement { + /** + * charge时,电量背景处会冒出气泡 + */ + @property({type: Boolean, reflect: true}) charge!: boolean + + @property({type: String}) theme!: BatteryThemeType + + /** + * 百分比数字区间: [0, 100] + */ + @property({type: Number}) percents = 100 + + @state() lowpower = false + + /** + * 分成三段, 小于半径20对应的电量比例,大于(436-半径20)对应的电量比例,中间段 + * + * 满电量: 'M0,20 a20 20, 0, 0, 1, 20,-20 h396 a20 20, 0, 0, 1, 20,20 v120 a20 20, 0, 0, 1, -20,20 h-396 a20 20, 0, 0, 1, -20,-20Z' + */ + private computeBatteryPath = (batteryPercents: number) => { + const [low, high] = [ + Math.floor((20 * 100) / 436), + Math.ceil((416 * 100) / 436), + ] + console.log(batteryPercents, typeof batteryPercents) + + if (batteryPercents >= high) { + const dx = (436 * batteryPercents) / 100 - 20 - 396 + const dy = Math.sqrt(400 - dx * dx) + const verticalHeight = 160 - (20 - dy) * 2 + return `M0,20 a20 20, 0, 0, 1, 20,-20 h396 a20 20, 0, 0, 1, ${dx},${ + 20 - dy + } v${verticalHeight} a20 20, 0, 0, 1, -${dx},${ + 20 - dy + } h-396 a20 20, 0, 0, 1, -20,-20Z` + } else if (batteryPercents <= low) { + const dx = (436 * batteryPercents) / 100 + const dy = Math.sqrt(400 - (20 - dx) * (20 - dx)) + const verticalHeight = 160 - (20 - dy) * 2 + return `M0,20 a20 20, 0, 0, 1, ${dx},-${dy} h0 a20 20, 0, 0, 1, 0,-0 v${verticalHeight} a20 20, 0, 0, 1, -${dx},-${dy} h-0 a20 20, 0, 0, 1, -20,-20Z` + } else { + const horizontalWidth = (436 * batteryPercents) / 100 - 20 + return `M0,20 a20 20, 0, 0, 1, 20,-20 h${horizontalWidth} a20 20, 0, 0, 1, 0,0 v160 a20 20, 0, 0, 1, 0,0 h-${horizontalWidth} a20 20, 0, 0, 1, -20,-20Z` + } + } + + attributeChangedCallback(name: string, _old: string, value: string) { + super.attributeChangedCallback(name, _old, value) + if (name === 'percents') { + const _value = Number(value) + if (_value > 100 || _value < 0) { + throw new Error('Set wrong number on battery-icons percents') + } else { + this.lowpower = _value < 20 + } + } + } + + render() { + return html` + + + + + + + + + + + + + + + + + + + + + + + + + + ${this.percents} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` + } + + static styles = css` + :host { + width: 100%; + height: 100%; + } + + svg { + --text-color: white; + --battery-bg: url(#linear_green); + --card-bg: url(#linear_white); + } + + svg[theme='dm'] { + --card-bg: url(#linear_black); + } + + svg[lowpower] { + --text-color: #ebc883; + --battery-bg: url(#linear_red); + } + + svg:not([charge]) #charge-bubble { + display: none; + } + + @media (prefers-color-scheme: dark) { + svg { + --card-bg: url(#linear_black); + } + } + ` +} + +declare global { + interface HTMLElementTagNameMap { + 'battery-rect-card': BatteryRectCard + } +} diff --git a/src/components/battery/rect-card/test-horizontal.svg b/src/components/battery/rect-card/test-horizontal.svg new file mode 100644 index 0000000..39254f6 --- /dev/null +++ b/src/components/battery/rect-card/test-horizontal.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/battery/rect-card/test-vertical.svg b/src/components/battery/rect-card/test-vertical.svg new file mode 100644 index 0000000..a00b1cd --- /dev/null +++ b/src/components/battery/rect-card/test-vertical.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/panels/battery/battery.ts b/src/test/panels/battery/battery.ts index 9151bb0..b32196a 100644 --- a/src/test/panels/battery/battery.ts +++ b/src/test/panels/battery/battery.ts @@ -1,40 +1,132 @@ -import {html, LitElement, CSSResultArray, css, PropertyValueMap} from 'lit' -import {customElement, query} from 'lit/decorators.js' -import {sharedStyles} from '../../../components/battery/battery-styles' +import {html, LitElement, css} from 'lit' +import {customElement, query, state} from 'lit/decorators.js' +import '../../../components/battery/rect-card/rect-card' +import '../../../components/battery/dynamic-icon/icon' +import {BatteryDynamicIcon} from '../../../components/battery/dynamic-icon/icon' +import {BatteryMode, BatteryThemeType} from '../../../components/battery/base' @customElement('panel-battery') export class PanelBattery extends LitElement { - @query('#container') container!: HTMLElement - @query('star-battery') battery!: HTMLElement + @query('battery-dynamic-icon') batteryDynamicIcon!: BatteryDynamicIcon + + constructor() { + super() + + const _addEventListener = Element.prototype.addEventListener + + Element.prototype.addEventListener = function ( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ) { + console.log(this, type) + _addEventListener.call(this, type, listener, options) + } + } + + @state() percents = 100 + + @state() theme!: BatteryThemeType + + @state() percentage!: Boolean + + @state() mode!: BatteryMode + + @state() charge!: Boolean + render() { return html` -
- +
+ + { + const bool = e.target.checked + const parent = e.target.parentElement + parent.description = bool ? '省电模式' : '常规模式' + this.mode = bool ? 'power-save' : 'power-normal' + }} + > + + + + { + const bool = e.target.checked + const parent = e.target.parentElement + parent.description = bool ? '充电' : '未充电' + this.charge = bool + }} + > + + + (this.percentage = true)} + @disabled=${() => (this.percentage = false)} + > + + (this.theme = 'dm')} + @disabled=${() => (this.theme = 'lm')} + > + + + (this.percents = e.detail.value)} + > + +
+ +
+ +
` } - protected firstUpdated( - _changedProperties: PropertyValueMap | Map - ): void { - ;(window as any).battery = this.battery - } - - public static override get styles(): CSSResultArray { - return [ - sharedStyles, - css` - :host { - height: 100vh; - width: 100vw; - } - #container { - height: 60vh; - width: 50vw; - } - `, - ] - } + static styles = css` + :host { + display: flex; + flex-direction: column; + } + battery-dynamic-icon, + battery-rect-card { + margin: auto; + } + battery-rect-card { + width: 500px; + } + battery-dynamic-icon { + width: 250px; + } + ` } declare global { diff --git a/src/widgets/battery/battery.ts b/src/widgets/battery/battery.ts index 5f3252b..b981915 100644 --- a/src/widgets/battery/battery.ts +++ b/src/widgets/battery/battery.ts @@ -1,11 +1,26 @@ -import GaiaWidget from '../gaia-widget' -import '../../components/battery/battery' -import {StarBattery} from '../../components/battery/battery' -import {customElement, query} from 'lit/decorators.js' import {css, html} from 'lit' +import {customElement, query, state} from 'lit/decorators.js' +import GaiaWidget from '../gaia-widget' +import '../../components/battery/rect-card/rect-card' +import {BatteryRectCard} from '../../components/battery/rect-card/rect-card' +import {BatteryManager} from '../../components/battery/base' + +declare global { + interface Navigator { + getBattery: () => Promise + } +} @customElement('gaia-battery') class BatteryWidget extends GaiaWidget { + @query('battery-rect-card') batteryRectCard!: BatteryRectCard + + @state() charge = false + + @state() percents = 100 + + batteryManager!: BatteryManager + constructor({ url, appName, @@ -27,25 +42,22 @@ class BatteryWidget extends GaiaWidget { url: url || 'js/widgets/battery.js', appName: appName || 'homescreen', origin: origin || 'http://homescreen.localhost/manifest.webmanifest', - size: size || [2, 2], + size: size || [1, 2], widgetType: widgetType || 'battery', widgetName, params, }) } - @query('star-battery') battery!: StarBattery - _battery: any init = async () => { - // @ts-ignore - this._battery = await navigator.getBattery() - this.percent = this._battery.level * 100 - this.battery.charge = this._battery.charging - this._battery.addEventListener('levelchange', this) - this._battery.addEventListener('chargingchange', this) + this.batteryManager = await navigator.getBattery() + + this.percents = this.batteryManager.level * 100 + this.charge = this.batteryManager.charging this.lifeCycle = 'initialized' - this.percent = this._battery.level * 100 - this.battery.charge = this._battery.charging + + this.batteryManager.addEventListener('levelchange', this) + this.batteryManager.addEventListener('chargingchange', this) } firstUpdated = async () => { @@ -55,41 +67,23 @@ class BatteryWidget extends GaiaWidget { handleEvent(event: Event): void { switch (event.type) { case 'levelchange': - this.percent = this._battery.level * 100 + this.percents = this.batteryManager.level * 100 break case 'chargingchange': - this.battery.charge = this._battery.charging + this.charge = this.batteryManager.charging break } } - get percent() { - return this.battery.percent - } - - set percent(value) { - this.battery.percent = value - } - - get charge() { - return this.battery.charge - } - - set charge(value) { - // this.battery.charge = !!value - if (value) { - setTimeout(() => { - this.battery.charge = false - }) - } - this.battery.charge = value - } - render() { return html` - + ` } + static override get styles() { return [ css`