381 lines
11 KiB
JavaScript
Executable File
381 lines
11 KiB
JavaScript
Executable File
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import GObject from 'gi://GObject';
|
|
import St from 'gi://St';
|
|
|
|
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
|
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
|
|
|
import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
import {getIcon} from './utils.js';
|
|
|
|
import * as GMenu from './gmenu.js';
|
|
import Tooltip from './tooltip.js';
|
|
|
|
|
|
/**
|
|
* A battery widget with an icon, text percentage and time estimate tooltip
|
|
*/
|
|
export const Battery = GObject.registerClass({
|
|
GTypeName: 'GSConnectShellDeviceBattery',
|
|
}, class Battery extends St.BoxLayout {
|
|
|
|
_init(params) {
|
|
super._init({
|
|
reactive: true,
|
|
style_class: 'gsconnect-device-battery',
|
|
track_hover: true,
|
|
});
|
|
Object.assign(this, params);
|
|
|
|
// Percent Label
|
|
this.label = new St.Label({
|
|
y_align: Clutter.ActorAlign.CENTER,
|
|
});
|
|
this.label.clutter_text.ellipsize = 0;
|
|
this.add_child(this.label);
|
|
|
|
// Battery Icon
|
|
this.icon = new St.Icon({
|
|
fallback_icon_name: 'battery-missing-symbolic',
|
|
icon_size: 16,
|
|
});
|
|
this.add_child(this.icon);
|
|
|
|
// Battery Estimate
|
|
this.tooltip = new Tooltip({
|
|
parent: this,
|
|
text: null,
|
|
});
|
|
|
|
// Battery GAction
|
|
this._actionAddedId = this.device.action_group.connect(
|
|
'action-added',
|
|
this._onActionChanged.bind(this)
|
|
);
|
|
this._actionRemovedId = this.device.action_group.connect(
|
|
'action-removed',
|
|
this._onActionChanged.bind(this)
|
|
);
|
|
this._actionStateChangedId = this.device.action_group.connect(
|
|
'action-state-changed',
|
|
this._onStateChanged.bind(this)
|
|
);
|
|
|
|
this._onActionChanged(this.device.action_group, 'battery');
|
|
|
|
// Cleanup on destroy
|
|
this.connect('destroy', this._onDestroy);
|
|
}
|
|
|
|
_onActionChanged(action_group, action_name) {
|
|
if (action_name !== 'battery')
|
|
return;
|
|
|
|
if (action_group.has_action('battery')) {
|
|
const value = action_group.get_action_state('battery');
|
|
const [charging, icon_name, level, time] = value.deepUnpack();
|
|
|
|
this._state = {
|
|
charging: charging,
|
|
icon_name: icon_name,
|
|
level: level,
|
|
time: time,
|
|
};
|
|
} else {
|
|
this._state = null;
|
|
}
|
|
|
|
this._sync();
|
|
}
|
|
|
|
_onStateChanged(action_group, action_name, value) {
|
|
if (action_name !== 'battery')
|
|
return;
|
|
|
|
const [charging, icon_name, level, time] = value.deepUnpack();
|
|
|
|
this._state = {
|
|
charging: charging,
|
|
icon_name: icon_name,
|
|
level: level,
|
|
time: time,
|
|
};
|
|
|
|
this._sync();
|
|
}
|
|
|
|
_getBatteryLabel() {
|
|
if (!this._state)
|
|
return null;
|
|
|
|
const {charging, level, time} = this._state;
|
|
|
|
if (level === 100)
|
|
// TRANSLATORS: When the battery level is 100%
|
|
return _('Fully Charged');
|
|
|
|
if (time === 0)
|
|
// TRANSLATORS: When no time estimate for the battery is available
|
|
// EXAMPLE: 42% (Estimating…)
|
|
return _('%d%% (Estimating…)').format(level);
|
|
|
|
const total = time / 60;
|
|
const minutes = Math.floor(total % 60);
|
|
const hours = Math.floor(total / 60);
|
|
|
|
if (charging) {
|
|
// TRANSLATORS: Estimated time until battery is charged
|
|
// EXAMPLE: 42% (1:15 Until Full)
|
|
return _('%d%% (%d\u2236%02d Until Full)').format(
|
|
level,
|
|
hours,
|
|
minutes
|
|
);
|
|
} else {
|
|
// TRANSLATORS: Estimated time until battery is empty
|
|
// EXAMPLE: 42% (12:15 Remaining)
|
|
return _('%d%% (%d\u2236%02d Remaining)').format(
|
|
level,
|
|
hours,
|
|
minutes
|
|
);
|
|
}
|
|
}
|
|
|
|
_onDestroy(actor) {
|
|
actor.device.action_group.disconnect(actor._actionAddedId);
|
|
actor.device.action_group.disconnect(actor._actionRemovedId);
|
|
actor.device.action_group.disconnect(actor._actionStateChangedId);
|
|
}
|
|
|
|
_sync() {
|
|
this.visible = !!this._state;
|
|
|
|
if (!this.visible)
|
|
return;
|
|
|
|
this.icon.icon_name = this._state.icon_name;
|
|
this.label.text = (this._state.level > -1) ? `${this._state.level}%` : '';
|
|
this.tooltip.text = this._getBatteryLabel();
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* A cell signal strength widget with two icons
|
|
*/
|
|
export const SignalStrength = GObject.registerClass({
|
|
GTypeName: 'GSConnectShellDeviceSignalStrength',
|
|
}, class SignalStrength extends St.BoxLayout {
|
|
|
|
_init(params) {
|
|
super._init({
|
|
reactive: true,
|
|
style_class: 'gsconnect-device-signal-strength',
|
|
track_hover: true,
|
|
});
|
|
Object.assign(this, params);
|
|
|
|
// Network Type Icon
|
|
this.networkTypeIcon = new St.Icon({
|
|
fallback_icon_name: 'network-cellular-symbolic',
|
|
icon_size: 16,
|
|
});
|
|
this.add_child(this.networkTypeIcon);
|
|
|
|
// Signal Strength Icon
|
|
this.signalStrengthIcon = new St.Icon({
|
|
fallback_icon_name: 'network-cellular-offline-symbolic',
|
|
icon_size: 16,
|
|
});
|
|
this.add_child(this.signalStrengthIcon);
|
|
|
|
// Network Type Text
|
|
this.tooltip = new Tooltip({
|
|
parent: this,
|
|
text: null,
|
|
});
|
|
|
|
// ConnectivityReport GAction
|
|
this._actionAddedId = this.device.action_group.connect(
|
|
'action-added',
|
|
this._onActionChanged.bind(this)
|
|
);
|
|
this._actionRemovedId = this.device.action_group.connect(
|
|
'action-removed',
|
|
this._onActionChanged.bind(this)
|
|
);
|
|
this._actionStateChangedId = this.device.action_group.connect(
|
|
'action-state-changed',
|
|
this._onStateChanged.bind(this)
|
|
);
|
|
|
|
this._onActionChanged(this.device.action_group, 'connectivityReport');
|
|
|
|
// Cleanup on destroy
|
|
this.connect('destroy', this._onDestroy);
|
|
}
|
|
|
|
_onActionChanged(action_group, action_name) {
|
|
if (action_name !== 'connectivityReport')
|
|
return;
|
|
|
|
if (action_group.has_action('connectivityReport')) {
|
|
const value = action_group.get_action_state('connectivityReport');
|
|
const [
|
|
cellular_network_type,
|
|
cellular_network_type_icon,
|
|
cellular_network_strength,
|
|
cellular_network_strength_icon,
|
|
hotspot_name,
|
|
hotspot_bssid,
|
|
] = value.deepUnpack();
|
|
|
|
this._state = {
|
|
cellular_network_type: cellular_network_type,
|
|
cellular_network_type_icon: cellular_network_type_icon,
|
|
cellular_network_strength: cellular_network_strength,
|
|
cellular_network_strength_icon: cellular_network_strength_icon,
|
|
hotspot_name: hotspot_name,
|
|
hotspot_bssid: hotspot_bssid,
|
|
};
|
|
} else {
|
|
this._state = null;
|
|
}
|
|
|
|
this._sync();
|
|
}
|
|
|
|
_onStateChanged(action_group, action_name, value) {
|
|
if (action_name !== 'connectivityReport')
|
|
return;
|
|
|
|
const [
|
|
cellular_network_type,
|
|
cellular_network_type_icon,
|
|
cellular_network_strength,
|
|
cellular_network_strength_icon,
|
|
hotspot_name,
|
|
hotspot_bssid,
|
|
] = value.deepUnpack();
|
|
|
|
this._state = {
|
|
cellular_network_type: cellular_network_type,
|
|
cellular_network_type_icon: cellular_network_type_icon,
|
|
cellular_network_strength: cellular_network_strength,
|
|
cellular_network_strength_icon: cellular_network_strength_icon,
|
|
hotspot_name: hotspot_name,
|
|
hotspot_bssid: hotspot_bssid,
|
|
};
|
|
|
|
this._sync();
|
|
}
|
|
|
|
_onDestroy(actor) {
|
|
actor.device.action_group.disconnect(actor._actionAddedId);
|
|
actor.device.action_group.disconnect(actor._actionRemovedId);
|
|
actor.device.action_group.disconnect(actor._actionStateChangedId);
|
|
}
|
|
|
|
_sync() {
|
|
this.visible = !!this._state;
|
|
|
|
if (!this.visible)
|
|
return;
|
|
|
|
this.networkTypeIcon.icon_name = this._state.cellular_network_type_icon;
|
|
this.signalStrengthIcon.icon_name = this._state.cellular_network_strength_icon;
|
|
this.tooltip.text = this._state.cellular_network_type;
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* A PopupMenu used as an information and control center for a device
|
|
*/
|
|
export class Menu extends PopupMenu.PopupMenuSection {
|
|
|
|
constructor(params) {
|
|
super();
|
|
Object.assign(this, params);
|
|
|
|
this.actor.add_style_class_name('gsconnect-device-menu');
|
|
|
|
// Title
|
|
this._title = new PopupMenu.PopupSeparatorMenuItem(this.device.name);
|
|
this.addMenuItem(this._title);
|
|
|
|
// Title -> Name
|
|
this._title.label.style_class = 'gsconnect-device-name';
|
|
this._title.label.clutter_text.ellipsize = 0;
|
|
this.device.bind_property(
|
|
'name',
|
|
this._title.label,
|
|
'text',
|
|
GObject.BindingFlags.SYNC_CREATE
|
|
);
|
|
|
|
// Title -> Cellular Signal Strength
|
|
this._signalStrength = new SignalStrength({device: this.device});
|
|
this._title.actor.add_child(this._signalStrength);
|
|
|
|
// Title -> Battery
|
|
this._battery = new Battery({device: this.device});
|
|
this._title.actor.add_child(this._battery);
|
|
|
|
// Actions
|
|
let actions;
|
|
|
|
if (this.menu_type === 'icon') {
|
|
actions = new GMenu.IconBox({
|
|
action_group: this.device.action_group,
|
|
model: this.device.menu,
|
|
});
|
|
} else if (this.menu_type === 'list') {
|
|
actions = new GMenu.ListBox({
|
|
action_group: this.device.action_group,
|
|
model: this.device.menu,
|
|
});
|
|
}
|
|
|
|
this.addMenuItem(actions);
|
|
}
|
|
|
|
isEmpty() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* An indicator representing a Device in the Status Area
|
|
*/
|
|
export const Indicator = GObject.registerClass({
|
|
GTypeName: 'GSConnectDeviceIndicator',
|
|
}, class Indicator extends PanelMenu.Button {
|
|
|
|
_init(params) {
|
|
super._init(0.0, `${params.device.name} Indicator`, false);
|
|
Object.assign(this, params);
|
|
|
|
// Device Icon
|
|
this._icon = new St.Icon({
|
|
gicon: getIcon(this.device.icon_name),
|
|
style_class: 'system-status-icon gsconnect-device-indicator',
|
|
});
|
|
this.add_child(this._icon);
|
|
|
|
// Menu
|
|
const menu = new Menu({
|
|
device: this.device,
|
|
menu_type: 'icon',
|
|
});
|
|
this.menu.addMenuItem(menu);
|
|
}
|
|
});
|
|
|