956 lines
25 KiB
TypeScript
956 lines
25 KiB
TypeScript
|
|
import {
|
||
|
|
customElement,
|
||
|
|
DeesElement,
|
||
|
|
type TemplateResult,
|
||
|
|
html,
|
||
|
|
property,
|
||
|
|
css,
|
||
|
|
cssManager,
|
||
|
|
state,
|
||
|
|
} from '@design.estate/dees-element';
|
||
|
|
import { DeesIcon } from '@design.estate/dees-catalog';
|
||
|
|
import { demo } from './eco-applauncher.demo.js';
|
||
|
|
import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
|
||
|
|
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
|
||
|
|
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
|
||
|
|
|
||
|
|
// Ensure components are registered
|
||
|
|
DeesIcon;
|
||
|
|
EcoApplauncherWifimenu;
|
||
|
|
EcoApplauncherBatterymenu;
|
||
|
|
EcoApplauncherSoundmenu;
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface HTMLElementTagNameMap {
|
||
|
|
'eco-applauncher': EcoApplauncher;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IAppIcon {
|
||
|
|
name: string;
|
||
|
|
icon: string;
|
||
|
|
action?: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface IStatusBarConfig {
|
||
|
|
showTime?: boolean;
|
||
|
|
showNetwork?: boolean;
|
||
|
|
showBattery?: boolean;
|
||
|
|
showSound?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ITopBarConfig {
|
||
|
|
showSearch?: boolean;
|
||
|
|
showDate?: boolean;
|
||
|
|
showNotifications?: boolean;
|
||
|
|
showUser?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export type TNetworkStatus = 'online' | 'offline' | 'connecting';
|
||
|
|
export type TBatteryStatus = number | 'charging';
|
||
|
|
|
||
|
|
@customElement('eco-applauncher')
|
||
|
|
export class EcoApplauncher extends DeesElement {
|
||
|
|
public static demo = demo;
|
||
|
|
public static demoGroup = 'App Launcher';
|
||
|
|
|
||
|
|
public static styles = [
|
||
|
|
cssManager.defaultStyles,
|
||
|
|
css`
|
||
|
|
:host {
|
||
|
|
display: block;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 20% 97%)', 'hsl(240 10% 4%)')};
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
|
||
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.launcher-container {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-bar {
|
||
|
|
height: 48px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')};
|
||
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0 24px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-left {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-center {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-right {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-date {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-box {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 8px 16px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 12%)')};
|
||
|
|
border-radius: 20px;
|
||
|
|
min-width: 200px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-box:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 86%)', 'hsl(240 5% 15%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-box dees-icon {
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.search-text {
|
||
|
|
font-size: 14px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-icon-button {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
width: 36px;
|
||
|
|
height: 36px;
|
||
|
|
border-radius: 50%;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.15s ease;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.top-icon-button:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-avatar {
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
color: white;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 600;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: transform 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.user-avatar:hover {
|
||
|
|
transform: scale(1.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
.notification-badge {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.notification-badge .badge {
|
||
|
|
position: absolute;
|
||
|
|
top: -4px;
|
||
|
|
right: -4px;
|
||
|
|
min-width: 16px;
|
||
|
|
height: 16px;
|
||
|
|
padding: 0 4px;
|
||
|
|
background: hsl(0 72% 51%);
|
||
|
|
color: white;
|
||
|
|
font-size: 10px;
|
||
|
|
font-weight: 600;
|
||
|
|
border-radius: 8px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.apps-area {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 48px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.apps-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||
|
|
gap: 32px;
|
||
|
|
max-width: 800px;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
padding: 16px;
|
||
|
|
border-radius: 16px;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: background 0.2s ease, transform 0.15s ease;
|
||
|
|
user-select: none;
|
||
|
|
-webkit-tap-highlight-color: transparent;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 12%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon:active {
|
||
|
|
transform: scale(0.95);
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 16%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon-circle {
|
||
|
|
width: 64px;
|
||
|
|
height: 64px;
|
||
|
|
border-radius: 16px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 15%)')};
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 80%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon-circle dees-icon {
|
||
|
|
--dees-icon-size: 28px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon-name {
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 85%)')};
|
||
|
|
text-align: center;
|
||
|
|
max-width: 90px;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
white-space: nowrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-bar {
|
||
|
|
height: 48px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')};
|
||
|
|
border-top: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: 0 24px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-left {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-right {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
font-size: 13px;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-icon {
|
||
|
|
font-size: 16px;
|
||
|
|
opacity: 0.8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-time {
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-indicator {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-bar {
|
||
|
|
width: 24px;
|
||
|
|
height: 12px;
|
||
|
|
border: 1.5px solid ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
border-radius: 3px;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-bar::after {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
right: -4px;
|
||
|
|
top: 50%;
|
||
|
|
transform: translateY(-50%);
|
||
|
|
width: 2px;
|
||
|
|
height: 6px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
|
||
|
|
border-radius: 0 1px 1px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-fill {
|
||
|
|
height: 100%;
|
||
|
|
background: hsl(142 71% 45%);
|
||
|
|
transition: width 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-fill.low {
|
||
|
|
background: hsl(0 72% 51%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.battery-fill.charging {
|
||
|
|
background: hsl(47 100% 50%);
|
||
|
|
}
|
||
|
|
|
||
|
|
.network-indicator {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-end;
|
||
|
|
gap: 2px;
|
||
|
|
height: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.network-bar {
|
||
|
|
width: 3px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 75%)', 'hsl(0 0% 40%)')};
|
||
|
|
border-radius: 1px;
|
||
|
|
transition: background 0.2s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.network-bar.active {
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.network-bar:nth-child(1) { height: 4px; }
|
||
|
|
.network-bar:nth-child(2) { height: 7px; }
|
||
|
|
.network-bar:nth-child(3) { height: 10px; }
|
||
|
|
.network-bar:nth-child(4) { height: 14px; }
|
||
|
|
|
||
|
|
.sound-indicator {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sound-bars {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-end;
|
||
|
|
gap: 2px;
|
||
|
|
height: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sound-bar {
|
||
|
|
width: 2px;
|
||
|
|
background: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 90%)')};
|
||
|
|
border-radius: 1px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sound-bar:nth-child(1) { height: 4px; }
|
||
|
|
.sound-bar:nth-child(2) { height: 8px; }
|
||
|
|
.sound-bar:nth-child(3) { height: 12px; }
|
||
|
|
|
||
|
|
.status-item-wrapper {
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-item.clickable {
|
||
|
|
cursor: pointer;
|
||
|
|
padding: 4px 8px;
|
||
|
|
margin: -4px -8px;
|
||
|
|
border-radius: 6px;
|
||
|
|
transition: background 0.15s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-item.clickable:hover {
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-item.clickable.active {
|
||
|
|
background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')};
|
||
|
|
}
|
||
|
|
|
||
|
|
.menu-popup {
|
||
|
|
position: absolute;
|
||
|
|
bottom: calc(100% + 8px);
|
||
|
|
left: 0;
|
||
|
|
z-index: 100;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 600px) {
|
||
|
|
.apps-area {
|
||
|
|
padding: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.apps-grid {
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon-circle {
|
||
|
|
width: 56px;
|
||
|
|
height: 56px;
|
||
|
|
font-size: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.app-icon-name {
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-bar {
|
||
|
|
padding: 0 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-left,
|
||
|
|
.status-right {
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
];
|
||
|
|
|
||
|
|
@property({ type: Array })
|
||
|
|
accessor apps: IAppIcon[] = [];
|
||
|
|
|
||
|
|
@property({ type: Object })
|
||
|
|
accessor statusConfig: IStatusBarConfig = {
|
||
|
|
showTime: true,
|
||
|
|
showNetwork: true,
|
||
|
|
showBattery: true,
|
||
|
|
showSound: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
@property({ type: Object })
|
||
|
|
accessor topBarConfig: ITopBarConfig = {
|
||
|
|
showSearch: true,
|
||
|
|
showDate: true,
|
||
|
|
showNotifications: true,
|
||
|
|
showUser: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor userName = 'User';
|
||
|
|
|
||
|
|
@property({ type: Number })
|
||
|
|
accessor notificationCount = 0;
|
||
|
|
|
||
|
|
@property({ type: Number })
|
||
|
|
accessor batteryLevel: TBatteryStatus = 100;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor networkStatus: TNetworkStatus = 'online';
|
||
|
|
|
||
|
|
@property({ type: Number })
|
||
|
|
accessor soundLevel = 50;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor currentTime = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor currentDate = '';
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor wifiMenuOpen = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor batteryMenuOpen = false;
|
||
|
|
|
||
|
|
@state()
|
||
|
|
accessor soundMenuOpen = false;
|
||
|
|
|
||
|
|
@property({ type: Array })
|
||
|
|
accessor networks: IWifiNetwork[] = [];
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor connectedNetwork: string | null = null;
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor wifiEnabled = true;
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor isCharging = false;
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor batterySaverEnabled = false;
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor timeRemaining: string | null = null;
|
||
|
|
|
||
|
|
@property({ type: Array })
|
||
|
|
accessor outputDevices: IAudioDevice[] = [];
|
||
|
|
|
||
|
|
@property({ type: String })
|
||
|
|
accessor activeDeviceId: string | null = null;
|
||
|
|
|
||
|
|
@property({ type: Boolean })
|
||
|
|
accessor muted = false;
|
||
|
|
|
||
|
|
private timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
this.updateTime();
|
||
|
|
}
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="launcher-container">
|
||
|
|
${this.renderTopBar()}
|
||
|
|
<div class="apps-area">
|
||
|
|
<div class="apps-grid">
|
||
|
|
${this.apps.map((app) => this.renderAppIcon(app))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="status-bar">
|
||
|
|
<div class="status-left">
|
||
|
|
${this.statusConfig.showNetwork ? this.renderNetworkStatusWithMenu() : ''}
|
||
|
|
${this.statusConfig.showBattery ? this.renderBatteryStatusWithMenu() : ''}
|
||
|
|
${this.statusConfig.showSound ? this.renderSoundStatusWithMenu() : ''}
|
||
|
|
</div>
|
||
|
|
<div class="status-right">
|
||
|
|
${this.statusConfig.showTime ? html`
|
||
|
|
<span class="status-time">${this.currentTime}</span>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderAppIcon(app: IAppIcon): TemplateResult {
|
||
|
|
return html`
|
||
|
|
<div class="app-icon" @click=${() => this.handleAppClick(app)}>
|
||
|
|
<div class="app-icon-circle">
|
||
|
|
<dees-icon .icon=${app.icon} .iconSize=${28}></dees-icon>
|
||
|
|
</div>
|
||
|
|
<span class="app-icon-name">${app.name}</span>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderTopBar(): TemplateResult {
|
||
|
|
const userInitials = this.userName
|
||
|
|
.split(' ')
|
||
|
|
.map((n) => n[0])
|
||
|
|
.join('')
|
||
|
|
.slice(0, 2)
|
||
|
|
.toUpperCase();
|
||
|
|
|
||
|
|
return html`
|
||
|
|
<div class="top-bar">
|
||
|
|
<div class="top-left">
|
||
|
|
${this.topBarConfig.showDate ? html`
|
||
|
|
<span class="top-date">${this.currentDate}</span>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
<div class="top-center">
|
||
|
|
${this.topBarConfig.showSearch ? html`
|
||
|
|
<div class="search-box" @click=${this.handleSearchClick}>
|
||
|
|
<dees-icon .icon=${'lucide:search'} .iconSize=${16}></dees-icon>
|
||
|
|
<span class="search-text">Search apps...</span>
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
<div class="top-right">
|
||
|
|
${this.topBarConfig.showNotifications ? html`
|
||
|
|
<div
|
||
|
|
class="top-icon-button notification-badge"
|
||
|
|
@click=${this.handleNotificationsClick}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${'lucide:bell'} .iconSize=${18}></dees-icon>
|
||
|
|
${this.notificationCount > 0 ? html`
|
||
|
|
<span class="badge">${this.notificationCount > 99 ? '99+' : this.notificationCount}</span>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
${this.topBarConfig.showUser ? html`
|
||
|
|
<div class="user-avatar" @click=${this.handleUserClick}>
|
||
|
|
${userInitials}
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderNetworkStatusWithMenu(): TemplateResult {
|
||
|
|
const bars = this.getNetworkBars();
|
||
|
|
return html`
|
||
|
|
<div class="status-item-wrapper">
|
||
|
|
<div
|
||
|
|
class="status-item clickable ${this.wifiMenuOpen ? 'active' : ''}"
|
||
|
|
@click=${this.handleNetworkClick}
|
||
|
|
>
|
||
|
|
<div class="network-indicator">
|
||
|
|
${[1, 2, 3, 4].map((bar) => html`
|
||
|
|
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="menu-popup">
|
||
|
|
<eco-applauncher-wifimenu
|
||
|
|
?open=${this.wifiMenuOpen}
|
||
|
|
.networks=${this.networks}
|
||
|
|
.connectedNetwork=${this.connectedNetwork}
|
||
|
|
.wifiEnabled=${this.wifiEnabled}
|
||
|
|
@menu-close=${this.handleWifiMenuClose}
|
||
|
|
@wifi-toggle=${this.handleWifiToggle}
|
||
|
|
@network-select=${this.handleNetworkSelect}
|
||
|
|
@settings-click=${this.handleWifiSettingsClick}
|
||
|
|
></eco-applauncher-wifimenu>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderNetworkStatus(): TemplateResult {
|
||
|
|
const bars = this.getNetworkBars();
|
||
|
|
return html`
|
||
|
|
<div class="status-item">
|
||
|
|
<div class="network-indicator">
|
||
|
|
${[1, 2, 3, 4].map((bar) => html`
|
||
|
|
<div class="network-bar ${bar <= bars ? 'active' : ''}"></div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderBatteryStatusWithMenu(): TemplateResult {
|
||
|
|
const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100;
|
||
|
|
const isCharging = this.batteryLevel === 'charging' || this.isCharging;
|
||
|
|
const isLow = level < 20;
|
||
|
|
|
||
|
|
return html`
|
||
|
|
<div class="status-item-wrapper">
|
||
|
|
<div
|
||
|
|
class="status-item battery-indicator clickable ${this.batteryMenuOpen ? 'active' : ''}"
|
||
|
|
@click=${this.handleBatteryClick}
|
||
|
|
>
|
||
|
|
<div class="battery-bar">
|
||
|
|
<div
|
||
|
|
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
|
||
|
|
style="width: ${level}%"
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
${isCharging
|
||
|
|
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
|
||
|
|
: html`<span>${level}%</span>`
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div class="menu-popup">
|
||
|
|
<eco-applauncher-batterymenu
|
||
|
|
?open=${this.batteryMenuOpen}
|
||
|
|
.batteryLevel=${level}
|
||
|
|
.isCharging=${isCharging}
|
||
|
|
.batterySaverEnabled=${this.batterySaverEnabled}
|
||
|
|
.timeRemaining=${this.timeRemaining}
|
||
|
|
@menu-close=${this.handleBatteryMenuClose}
|
||
|
|
@battery-saver-toggle=${this.handleBatterySaverToggle}
|
||
|
|
@settings-click=${this.handleBatterySettingsClick}
|
||
|
|
></eco-applauncher-batterymenu>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderBatteryStatus(): TemplateResult {
|
||
|
|
const level = typeof this.batteryLevel === 'number' ? this.batteryLevel : 100;
|
||
|
|
const isCharging = this.batteryLevel === 'charging';
|
||
|
|
const isLow = level < 20;
|
||
|
|
|
||
|
|
return html`
|
||
|
|
<div class="status-item battery-indicator">
|
||
|
|
<div class="battery-bar">
|
||
|
|
<div
|
||
|
|
class="battery-fill ${isLow ? 'low' : ''} ${isCharging ? 'charging' : ''}"
|
||
|
|
style="width: ${level}%"
|
||
|
|
></div>
|
||
|
|
</div>
|
||
|
|
${isCharging
|
||
|
|
? html`<dees-icon .icon=${'lucide:zap'} .iconSize=${14}></dees-icon>`
|
||
|
|
: html`<span>${level}%</span>`
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderSoundStatusWithMenu(): TemplateResult {
|
||
|
|
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
|
||
|
|
const soundIcon = this.muted || this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
|
||
|
|
|
||
|
|
return html`
|
||
|
|
<div class="status-item-wrapper">
|
||
|
|
<div
|
||
|
|
class="status-item sound-indicator clickable ${this.soundMenuOpen ? 'active' : ''}"
|
||
|
|
@click=${this.handleSoundClick}
|
||
|
|
>
|
||
|
|
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
|
||
|
|
<div class="sound-bars">
|
||
|
|
${[1, 2, 3].map((bar) => html`
|
||
|
|
<div
|
||
|
|
class="sound-bar"
|
||
|
|
style="opacity: ${bar <= activeBars && !this.muted ? 1 : 0.3}"
|
||
|
|
></div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="menu-popup">
|
||
|
|
<eco-applauncher-soundmenu
|
||
|
|
?open=${this.soundMenuOpen}
|
||
|
|
.volume=${this.soundLevel}
|
||
|
|
.muted=${this.muted}
|
||
|
|
.outputDevices=${this.outputDevices}
|
||
|
|
.activeDeviceId=${this.activeDeviceId}
|
||
|
|
@menu-close=${this.handleSoundMenuClose}
|
||
|
|
@volume-change=${this.handleVolumeChange}
|
||
|
|
@mute-toggle=${this.handleMuteToggle}
|
||
|
|
@device-select=${this.handleDeviceSelect}
|
||
|
|
@settings-click=${this.handleSoundSettingsClick}
|
||
|
|
></eco-applauncher-soundmenu>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderSoundStatus(): TemplateResult {
|
||
|
|
const activeBars = Math.ceil((this.soundLevel / 100) * 3);
|
||
|
|
const soundIcon = this.soundLevel === 0 ? 'lucide:volumeX' : 'lucide:volume2';
|
||
|
|
return html`
|
||
|
|
<div class="status-item sound-indicator">
|
||
|
|
<dees-icon .icon=${soundIcon} .iconSize=${16}></dees-icon>
|
||
|
|
<div class="sound-bars">
|
||
|
|
${[1, 2, 3].map((bar) => html`
|
||
|
|
<div
|
||
|
|
class="sound-bar"
|
||
|
|
style="opacity: ${bar <= activeBars ? 1 : 0.3}"
|
||
|
|
></div>
|
||
|
|
`)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private getNetworkBars(): number {
|
||
|
|
switch (this.networkStatus) {
|
||
|
|
case 'online':
|
||
|
|
return 4;
|
||
|
|
case 'connecting':
|
||
|
|
return 2;
|
||
|
|
case 'offline':
|
||
|
|
return 0;
|
||
|
|
default:
|
||
|
|
return 4;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleAppClick(app: IAppIcon): void {
|
||
|
|
this.dispatchEvent(
|
||
|
|
new CustomEvent('app-click', {
|
||
|
|
detail: { app },
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
if (app.action) {
|
||
|
|
app.action();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleNetworkClick(e: MouseEvent): void {
|
||
|
|
e.stopPropagation();
|
||
|
|
this.batteryMenuOpen = false;
|
||
|
|
this.soundMenuOpen = false;
|
||
|
|
this.wifiMenuOpen = !this.wifiMenuOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBatteryClick(e: MouseEvent): void {
|
||
|
|
e.stopPropagation();
|
||
|
|
this.wifiMenuOpen = false;
|
||
|
|
this.soundMenuOpen = false;
|
||
|
|
this.batteryMenuOpen = !this.batteryMenuOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleSoundClick(e: MouseEvent): void {
|
||
|
|
e.stopPropagation();
|
||
|
|
this.wifiMenuOpen = false;
|
||
|
|
this.batteryMenuOpen = false;
|
||
|
|
this.soundMenuOpen = !this.soundMenuOpen;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleWifiMenuClose(): void {
|
||
|
|
this.wifiMenuOpen = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBatteryMenuClose(): void {
|
||
|
|
this.batteryMenuOpen = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleSoundMenuClose(): void {
|
||
|
|
this.soundMenuOpen = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleVolumeChange(e: CustomEvent): void {
|
||
|
|
this.soundLevel = e.detail.volume;
|
||
|
|
this.dispatchEvent(new CustomEvent('volume-change', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleMuteToggle(e: CustomEvent): void {
|
||
|
|
this.muted = e.detail.muted;
|
||
|
|
this.dispatchEvent(new CustomEvent('mute-toggle', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleDeviceSelect(e: CustomEvent): void {
|
||
|
|
this.activeDeviceId = e.detail.device.id;
|
||
|
|
this.dispatchEvent(new CustomEvent('device-select', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleSoundSettingsClick(): void {
|
||
|
|
this.soundMenuOpen = false;
|
||
|
|
this.dispatchEvent(new CustomEvent('sound-settings-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleWifiToggle(e: CustomEvent): void {
|
||
|
|
this.wifiEnabled = e.detail.enabled;
|
||
|
|
this.dispatchEvent(new CustomEvent('wifi-toggle', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleNetworkSelect(e: CustomEvent): void {
|
||
|
|
this.dispatchEvent(new CustomEvent('network-select', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleWifiSettingsClick(): void {
|
||
|
|
this.wifiMenuOpen = false;
|
||
|
|
this.dispatchEvent(new CustomEvent('wifi-settings-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBatterySaverToggle(e: CustomEvent): void {
|
||
|
|
this.batterySaverEnabled = e.detail.enabled;
|
||
|
|
this.dispatchEvent(new CustomEvent('battery-saver-toggle', {
|
||
|
|
detail: e.detail,
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleBatterySettingsClick(): void {
|
||
|
|
this.batteryMenuOpen = false;
|
||
|
|
this.dispatchEvent(new CustomEvent('battery-settings-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleSearchClick(): void {
|
||
|
|
this.dispatchEvent(new CustomEvent('search-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleNotificationsClick(): void {
|
||
|
|
this.dispatchEvent(new CustomEvent('notifications-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private handleUserClick(): void {
|
||
|
|
this.dispatchEvent(new CustomEvent('user-click', {
|
||
|
|
bubbles: true,
|
||
|
|
composed: true,
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
private updateTime(): void {
|
||
|
|
const now = new Date();
|
||
|
|
const hours = now.getHours();
|
||
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
|
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||
|
|
const displayHours = hours % 12 || 12;
|
||
|
|
this.currentTime = `${displayHours}:${minutes} ${ampm}`;
|
||
|
|
|
||
|
|
// Update date
|
||
|
|
const options: Intl.DateTimeFormatOptions = {
|
||
|
|
weekday: 'short',
|
||
|
|
month: 'short',
|
||
|
|
day: 'numeric',
|
||
|
|
};
|
||
|
|
this.currentDate = now.toLocaleDateString('en-US', options);
|
||
|
|
}
|
||
|
|
|
||
|
|
async connectedCallback(): Promise<void> {
|
||
|
|
await super.connectedCallback();
|
||
|
|
this.updateTime();
|
||
|
|
this.timeUpdateInterval = setInterval(() => this.updateTime(), 1000);
|
||
|
|
}
|
||
|
|
|
||
|
|
async disconnectedCallback(): Promise<void> {
|
||
|
|
await super.disconnectedCallback();
|
||
|
|
if (this.timeUpdateInterval) {
|
||
|
|
clearInterval(this.timeUpdateInterval);
|
||
|
|
this.timeUpdateInterval = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|