123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gdk = imports.gi.Gdk;
- const GdkPixbuf = imports.gi.GdkPixbuf;
- const Gio = imports.gi.Gio;
- const GLib = imports.gi.GLib;
- const GObject = imports.gi.GObject;
- const Gtk = imports.gi.Gtk;
- const Config = imports.config;
- const Device = imports.preferences.device;
- const Remote = imports.utils.remote;
- /*
- * Header for support logs
- */
- const LOG_HEADER = new GLib.Bytes(`
- GSConnect: ${Config.PACKAGE_VERSION} (${Config.IS_USER ? 'user' : 'system'})
- GJS: ${imports.system.version}
- Session: ${GLib.getenv('XDG_SESSION_TYPE')}
- OS: ${GLib.get_os_info('PRETTY_NAME')}
- --------------------------------------------------------------------------------
- `);
- /**
- * Generate a support log.
- *
- * @param {string} time - Start time as a string (24-hour notation)
- */
- async function generateSupportLog(time) {
- try {
- const [file, stream] = Gio.File.new_tmp('gsconnect.XXXXXX');
- const logFile = stream.get_output_stream();
- await new Promise((resolve, reject) => {
- logFile.write_bytes_async(LOG_HEADER, 0, null, (file, res) => {
- try {
- resolve(file.write_bytes_finish(res));
- } catch (e) {
- reject(e);
- }
- });
- });
- // FIXME: BSD???
- const proc = new Gio.Subprocess({
- flags: (Gio.SubprocessFlags.STDOUT_PIPE |
- Gio.SubprocessFlags.STDERR_MERGE),
- argv: ['journalctl', '--no-host', '--since', time],
- });
- proc.init(null);
- logFile.splice_async(
- proc.get_stdout_pipe(),
- Gio.OutputStreamSpliceFlags.CLOSE_TARGET,
- GLib.PRIORITY_DEFAULT,
- null,
- (source, res) => {
- try {
- source.splice_finish(res);
- } catch (e) {
- logError(e);
- }
- }
- );
- await new Promise((resolve, reject) => {
- proc.wait_check_async(null, (proc, res) => {
- try {
- resolve(proc.wait_finish(res));
- } catch (e) {
- reject(e);
- }
- });
- });
- const uri = file.get_uri();
- Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
- } catch (e) {
- logError(e);
- }
- }
- /**
- * "Connect to..." Dialog
- */
- var ConnectDialog = GObject.registerClass({
- GTypeName: 'GSConnectConnectDialog',
- Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/connect-dialog.ui',
- Children: [
- 'cancel-button', 'connect-button',
- 'lan-grid', 'lan-ip', 'lan-port',
- ],
- }, class ConnectDialog extends Gtk.Dialog {
- _init(params = {}) {
- super._init(Object.assign({
- use_header_bar: true,
- }, params));
- }
- vfunc_response(response_id) {
- if (response_id === Gtk.ResponseType.OK) {
- try {
- let address;
- // Lan host/port entered
- if (this.lan_ip.text) {
- const host = this.lan_ip.text;
- const port = this.lan_port.value;
- address = GLib.Variant.new_string(`lan://${host}:${port}`);
- } else {
- return false;
- }
- this.application.activate_action('connect', address);
- } catch (e) {
- logError(e);
- }
- }
- this.destroy();
- return false;
- }
- });
- var Window = GObject.registerClass({
- GTypeName: 'GSConnectPreferencesWindow',
- Properties: {
- 'display-mode': GObject.ParamSpec.string(
- 'display-mode',
- 'Display Mode',
- 'Display devices in either the Panel or User Menu',
- GObject.ParamFlags.READWRITE,
- null
- ),
- },
- Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-window.ui',
- Children: [
- // HeaderBar
- 'headerbar', 'infobar', 'stack',
- 'service-menu', 'service-edit', 'refresh-button',
- 'device-menu', 'prev-button',
- // Popover
- 'rename-popover', 'rename', 'rename-label', 'rename-entry', 'rename-submit',
- // Focus Box
- 'service-window', 'service-box',
- // Device List
- 'device-list', 'device-list-spinner', 'device-list-placeholder',
- ],
- }, class PreferencesWindow extends Gtk.ApplicationWindow {
- _init(params = {}) {
- super._init(params);
- // Service Settings
- this.settings = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(
- 'org.gnome.Shell.Extensions.GSConnect',
- true
- ),
- });
- // Service Proxy
- this.service = new Remote.Service();
- this._deviceAddedId = this.service.connect(
- 'device-added',
- this._onDeviceAdded.bind(this)
- );
- this._deviceRemovedId = this.service.connect(
- 'device-removed',
- this._onDeviceRemoved.bind(this)
- );
- this._serviceChangedId = this.service.connect(
- 'notify::active',
- this._onServiceChanged.bind(this)
- );
- // HeaderBar (Service Name)
- this.headerbar.title = this.settings.get_string('name');
- this.rename_entry.text = this.headerbar.title;
- // Scroll with keyboard focus
- this.service_box.set_focus_vadjustment(this.service_window.vadjustment);
- // Device List
- this.device_list.set_header_func(Device.rowSeparators);
- // Discoverable InfoBar
- this.settings.bind(
- 'discoverable',
- this.infobar,
- 'reveal-child',
- Gio.SettingsBindFlags.INVERT_BOOLEAN
- );
- this.add_action(this.settings.create_action('discoverable'));
- // Application Menu
- this._initMenu();
- // Setting: Keep Alive When Locked
- this.add_action(this.settings.create_action('keep-alive-when-locked'));
- // Broadcast automatically every 5 seconds if there are no devices yet
- this._refreshSource = GLib.timeout_add_seconds(
- GLib.PRIORITY_DEFAULT,
- 5,
- this._refresh.bind(this)
- );
- // Restore window size/maximized/position
- this._restoreGeometry();
- // Prime the service
- this._initService();
- }
- get display_mode() {
- if (this.settings.get_boolean('show-indicators'))
- return 'panel';
- return 'user-menu';
- }
- set display_mode(mode) {
- this.settings.set_boolean('show-indicators', (mode === 'panel'));
- }
- vfunc_delete_event(event) {
- if (this.service) {
- this.service.disconnect(this._deviceAddedId);
- this.service.disconnect(this._deviceRemovedId);
- this.service.disconnect(this._serviceChangedId);
- this.service.destroy();
- this.service = null;
- }
- this._saveGeometry();
- GLib.source_remove(this._refreshSource);
- return false;
- }
- async _initService() {
- try {
- this.refresh_button.grab_focus();
- this._onServiceChanged(this.service, null);
- await this.service.reload();
- } catch (e) {
- logError(e, 'GSConnect');
- }
- }
- _initMenu() {
- // Panel/User Menu mode
- const displayMode = new Gio.PropertyAction({
- name: 'display-mode',
- property_name: 'display-mode',
- object: this,
- });
- this.add_action(displayMode);
- // About Dialog
- const aboutDialog = new Gio.SimpleAction({name: 'about'});
- aboutDialog.connect('activate', this._aboutDialog.bind(this));
- this.add_action(aboutDialog);
- // "Connect to..." Dialog
- const connectDialog = new Gio.SimpleAction({name: 'connect'});
- connectDialog.connect('activate', this._connectDialog.bind(this));
- this.add_action(connectDialog);
- // "Generate Support Log" GAction
- const generateSupportLog = new Gio.SimpleAction({name: 'support-log'});
- generateSupportLog.connect('activate', this._generateSupportLog.bind(this));
- this.add_action(generateSupportLog);
- // "Help" GAction
- const help = new Gio.SimpleAction({name: 'help'});
- help.connect('activate', this._help);
- this.add_action(help);
- }
- _refresh() {
- if (this.service.active && this.device_list.get_children().length < 1) {
- this.device_list_spinner.active = true;
- this.service.activate_action('refresh', null);
- } else {
- this.device_list_spinner.active = false;
- }
- return GLib.SOURCE_CONTINUE;
- }
- /*
- * Window State
- */
- _restoreGeometry() {
- this._windowState = new Gio.Settings({
- settings_schema: Config.GSCHEMA.lookup(
- 'org.gnome.Shell.Extensions.GSConnect.WindowState',
- true
- ),
- path: '/org/gnome/shell/extensions/gsconnect/preferences/',
- });
- // Size
- const [width, height] = this._windowState.get_value('window-size').deepUnpack();
- if (width && height)
- this.set_default_size(width, height);
- // Maximized State
- if (this._windowState.get_boolean('window-maximized'))
- this.maximize();
- }
- _saveGeometry() {
- const state = this.get_window().get_state();
- // Maximized State
- const maximized = (state & Gdk.WindowState.MAXIMIZED);
- this._windowState.set_boolean('window-maximized', maximized);
- // Leave the size at the value before maximizing
- if (maximized || (state & Gdk.WindowState.FULLSCREEN))
- return;
- // Size
- const size = this.get_size();
- this._windowState.set_value('window-size', new GLib.Variant('(ii)', size));
- }
- /**
- * About Dialog
- */
- _aboutDialog() {
- if (this._about === undefined) {
- this._about = new Gtk.AboutDialog({
- application: Gio.Application.get_default(),
- authors: [
- 'Andy Holmes <andrew.g.r.holmes@gmail.com>',
- 'Bertrand Lacoste <getzze@gmail.com>',
- 'Frank Dana <ferdnyc@gmail.com>',
- ],
- comments: _('A complete KDE Connect implementation for GNOME'),
- logo: GdkPixbuf.Pixbuf.new_from_resource_at_scale(
- '/org/gnome/Shell/Extensions/GSConnect/icons/org.gnome.Shell.Extensions.GSConnect.svg',
- 128,
- 128,
- true
- ),
- program_name: 'GSConnect',
- // TRANSLATORS: eg. 'Translator Name <your.email@domain.com>'
- translator_credits: _('translator-credits'),
- version: Config.PACKAGE_VERSION.toString(),
- website: Config.PACKAGE_URL,
- license_type: Gtk.License.GPL_2_0,
- modal: true,
- transient_for: this,
- });
- // Persist
- this._about.connect('response', (dialog) => dialog.hide_on_delete());
- this._about.connect('delete-event', (dialog) => dialog.hide_on_delete());
- }
- this._about.present();
- }
- /**
- * Connect to..." Dialog
- */
- _connectDialog() {
- new ConnectDialog({
- application: Gio.Application.get_default(),
- modal: true,
- transient_for: this,
- });
- }
- /*
- * "Generate Support Log" GAction
- */
- _generateSupportLog() {
- const dialog = new Gtk.MessageDialog({
- text: _('Generate Support Log'),
- secondary_text: _('Debug messages are being logged. Take any steps necessary to reproduce a problem then review the log.'),
- });
- dialog.add_button(_('Cancel'), Gtk.ResponseType.CANCEL);
- dialog.add_button(_('Review Log'), Gtk.ResponseType.OK);
- // Enable debug logging and mark the current time
- this.settings.set_boolean('debug', true);
- const now = GLib.DateTime.new_now_local().format('%R');
- dialog.connect('response', (dialog, response_id) => {
- // Disable debug logging and destroy the dialog
- this.settings.set_boolean('debug', false);
- dialog.destroy();
- // Only generate a log if instructed
- if (response_id === Gtk.ResponseType.OK)
- generateSupportLog(now);
- });
- dialog.show_all();
- }
- /*
- * "Help" GAction
- */
- _help(action, parameter) {
- const uri = `${Config.PACKAGE_URL}/wiki/Help`;
- Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
- }
- /*
- * HeaderBar Callbacks
- */
- _onPrevious(button, event) {
- // HeaderBar (Service)
- this.prev_button.visible = false;
- this.device_menu.visible = false;
- this.refresh_button.visible = true;
- this.service_edit.visible = true;
- this.service_menu.visible = true;
- this.headerbar.title = this.settings.get_string('name');
- this.headerbar.subtitle = null;
- // Panel
- this.stack.visible_child_name = 'service';
- this._setDeviceMenu();
- }
- _onEditServiceName(button, event) {
- this.rename_entry.text = this.headerbar.title;
- this.rename_entry.has_focus = true;
- }
- _onSetServiceName(widget) {
- if (this.rename_entry.text.length) {
- this.headerbar.title = this.rename_entry.text;
- this.settings.set_string('name', this.rename_entry.text);
- }
- this.service_edit.active = false;
- this.service_edit.grab_focus();
- }
- /*
- * Context Switcher
- */
- _getTypeLabel(device) {
- switch (device.type) {
- case 'laptop':
- return _('Laptop');
- case 'phone':
- return _('Smartphone');
- case 'tablet':
- return _('Tablet');
- case 'tv':
- return _('Television');
- default:
- return _('Desktop');
- }
- }
- _setDeviceMenu(panel = null) {
- this.device_menu.insert_action_group('device', null);
- this.device_menu.insert_action_group('settings', null);
- this.device_menu.set_menu_model(null);
- if (panel === null)
- return;
- this.device_menu.insert_action_group('device', panel.device.action_group);
- this.device_menu.insert_action_group('settings', panel.actions);
- this.device_menu.set_menu_model(panel.menu);
- }
- _onDeviceChanged(statusLabel, device, pspec) {
- switch (false) {
- case device.paired:
- statusLabel.label = _('Unpaired');
- break;
- case device.connected:
- statusLabel.label = _('Disconnected');
- break;
- default:
- statusLabel.label = _('Connected');
- }
- }
- _createDeviceRow(device) {
- const row = new Gtk.ListBoxRow({
- height_request: 52,
- selectable: false,
- visible: true,
- });
- row.set_name(device.id);
- const grid = new Gtk.Grid({
- column_spacing: 12,
- margin_left: 20,
- margin_right: 20,
- margin_bottom: 8,
- margin_top: 8,
- visible: true,
- });
- row.add(grid);
- const icon = new Gtk.Image({
- gicon: new Gio.ThemedIcon({name: device.icon_name}),
- icon_size: Gtk.IconSize.BUTTON,
- visible: true,
- });
- grid.attach(icon, 0, 0, 1, 1);
- const title = new Gtk.Label({
- halign: Gtk.Align.START,
- hexpand: true,
- valign: Gtk.Align.CENTER,
- vexpand: true,
- visible: true,
- });
- grid.attach(title, 1, 0, 1, 1);
- const status = new Gtk.Label({
- halign: Gtk.Align.END,
- hexpand: true,
- valign: Gtk.Align.CENTER,
- vexpand: true,
- visible: true,
- });
- grid.attach(status, 2, 0, 1, 1);
- // Keep name up to date
- device.bind_property(
- 'name',
- title,
- 'label',
- GObject.BindingFlags.SYNC_CREATE
- );
- // Keep status up to date
- device.connect(
- 'notify::connected',
- this._onDeviceChanged.bind(null, status)
- );
- device.connect(
- 'notify::paired',
- this._onDeviceChanged.bind(null, status)
- );
- this._onDeviceChanged(status, device, null);
- return row;
- }
- _onDeviceAdded(service, device) {
- try {
- if (!this.stack.get_child_by_name(device.id)) {
- // Add the device preferences
- const prefs = new Device.Panel(device);
- this.stack.add_titled(prefs, device.id, device.name);
- // Add a row to the device list
- prefs.row = this._createDeviceRow(device);
- this.device_list.add(prefs.row);
- }
- } catch (e) {
- logError(e);
- }
- }
- _onDeviceRemoved(service, device) {
- try {
- const prefs = this.stack.get_child_by_name(device.id);
- if (prefs === null)
- return;
- if (prefs === this.stack.get_visible_child())
- this._onPrevious();
- prefs.row.destroy();
- prefs.row = null;
- prefs.dispose();
- prefs.destroy();
- } catch (e) {
- logError(e);
- }
- }
- _onDeviceSelected(box, row) {
- try {
- if (row === null)
- return this._onPrevious();
- // Transition the panel
- const name = row.get_name();
- const prefs = this.stack.get_child_by_name(name);
- this.stack.visible_child = prefs;
- this._setDeviceMenu(prefs);
- // HeaderBar (Device)
- this.refresh_button.visible = false;
- this.service_edit.visible = false;
- this.service_menu.visible = false;
- this.prev_button.visible = true;
- this.device_menu.visible = true;
- this.headerbar.title = prefs.device.name;
- this.headerbar.subtitle = this._getTypeLabel(prefs.device);
- } catch (e) {
- logError(e);
- }
- }
- _onServiceChanged(service, pspec) {
- if (this.service.active)
- this.device_list_placeholder.label = _('Searching for devices…');
- else
- this.device_list_placeholder.label = _('Waiting for service…');
- }
- });
|