92 changed files with 24372 additions and 0 deletions
@ -0,0 +1,19 @@
|
||||
var PACKAGE_VERSION = 44; |
||||
var PACKAGE_URL = 'https://github.com/andyholmes/gnome-shell-extension-gsconnect'; |
||||
var PACKAGE_BUGREPORT = 'https://github.com/andyholmes/gnome-shell-extension-gsconnect/issues/new'; |
||||
var PACKAGE_DATADIR = '/usr/local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io'; |
||||
var PACKAGE_LOCALEDIR = '/usr/local/share/locale'; |
||||
var GSETTINGS_SCHEMA_DIR = '/usr/local/share/glib-2.0/schemas'; |
||||
var GNOME_SHELL_LIBDIR = '/usr/lib64/'; |
||||
|
||||
var APP_ID = 'org.gnome.Shell.Extensions.GSConnect'; |
||||
var APP_PATH = '/org/gnome/Shell/Extensions/GSConnect'; |
||||
|
||||
var IS_USER = false; |
||||
|
||||
// External binary paths
|
||||
var OPENSSL_PATH = 'openssl'; |
||||
var SSHADD_PATH = 'ssh-add'; |
||||
var SSHKEYGEN_PATH = 'ssh-keygen'; |
||||
var FFMPEG_PATH = 'ffmpeg'; |
||||
|
@ -0,0 +1,431 @@
|
||||
'use strict'; |
||||
|
||||
const Gio = imports.gi.Gio; |
||||
const GObject = imports.gi.GObject; |
||||
const Gtk = imports.gi.Gtk; |
||||
|
||||
const Main = imports.ui.main; |
||||
const PanelMenu = imports.ui.panelMenu; |
||||
const PopupMenu = imports.ui.popupMenu; |
||||
const AggregateMenu = Main.panel.statusArea.aggregateMenu; |
||||
|
||||
// Bootstrap
|
||||
const Extension = imports.misc.extensionUtils.getCurrentExtension(); |
||||
const Utils = Extension.imports.shell.utils; |
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
const _ = Extension._; |
||||
const Clipboard = Extension.imports.shell.clipboard; |
||||
const Config = Extension.imports.config; |
||||
const Device = Extension.imports.shell.device; |
||||
const Keybindings = Extension.imports.shell.keybindings; |
||||
const Notification = Extension.imports.shell.notification; |
||||
const Remote = Extension.imports.utils.remote; |
||||
|
||||
Extension.getIcon = Utils.getIcon; |
||||
|
||||
|
||||
/** |
||||
* A System Indicator used as the hub for spawning device indicators and |
||||
* indicating that the extension is active when there are none. |
||||
*/ |
||||
const ServiceIndicator = GObject.registerClass({ |
||||
GTypeName: 'GSConnectServiceIndicator', |
||||
}, class ServiceIndicator extends PanelMenu.SystemIndicator { |
||||
|
||||
_init() { |
||||
super._init(); |
||||
|
||||
this._menus = {}; |
||||
|
||||
this._keybindings = new Keybindings.Manager(); |
||||
|
||||
// GSettings
|
||||
this.settings = new Gio.Settings({ |
||||
settings_schema: Config.GSCHEMA.lookup( |
||||
'org.gnome.Shell.Extensions.GSConnect', |
||||
null |
||||
), |
||||
path: '/org/gnome/shell/extensions/gsconnect/', |
||||
}); |
||||
|
||||
this._enabledId = this.settings.connect( |
||||
'changed::enabled', |
||||
this._onEnabledChanged.bind(this) |
||||
); |
||||
|
||||
this._panelModeId = this.settings.connect( |
||||
'changed::show-indicators', |
||||
this._sync.bind(this) |
||||
); |
||||
|
||||
// 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) |
||||
); |
||||
|
||||
// Service Indicator
|
||||
this._indicator = this._addIndicator(); |
||||
this._indicator.gicon = Extension.getIcon( |
||||
'org.gnome.Shell.Extensions.GSConnect-symbolic' |
||||
); |
||||
this._indicator.visible = false; |
||||
|
||||
AggregateMenu._indicators.insert_child_at_index(this, 0); |
||||
AggregateMenu._gsconnect = this; |
||||
|
||||
// Service Menu
|
||||
this._item = new PopupMenu.PopupSubMenuMenuItem(_('Mobile Devices'), true); |
||||
this._item.icon.gicon = this._indicator.gicon; |
||||
this._item.label.clutter_text.x_expand = true; |
||||
this.menu.addMenuItem(this._item); |
||||
|
||||
// Find current index of network menu
|
||||
const menuItems = AggregateMenu.menu._getMenuItems(); |
||||
const networkMenuIndex = menuItems.indexOf(AggregateMenu._network.menu); |
||||
const menuIndex = networkMenuIndex > -1 ? networkMenuIndex : 3; |
||||
// Place our menu below the network menu
|
||||
AggregateMenu.menu.addMenuItem(this.menu, menuIndex + 1); |
||||
|
||||
// Service Menu -> Devices Section
|
||||
this.deviceSection = new PopupMenu.PopupMenuSection(); |
||||
this.deviceSection.actor.add_style_class_name('gsconnect-device-section'); |
||||
this.settings.bind( |
||||
'show-indicators', |
||||
this.deviceSection.actor, |
||||
'visible', |
||||
Gio.SettingsBindFlags.INVERT_BOOLEAN |
||||
); |
||||
this._item.menu.addMenuItem(this.deviceSection); |
||||
|
||||
// Service Menu -> Separator
|
||||
this._item.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); |
||||
|
||||
// Service Menu -> "Turn On/Off"
|
||||
this._enableItem = this._item.menu.addAction( |
||||
_('Turn On'), |
||||
this._enable.bind(this) |
||||
); |
||||
|
||||
// Service Menu -> "Mobile Settings"
|
||||
this._item.menu.addAction(_('Mobile Settings'), this._preferences); |
||||
|
||||
// Prime the service
|
||||
this._initService(); |
||||
} |
||||
|
||||
async _initService() { |
||||
try { |
||||
if (this.settings.get_boolean('enabled')) |
||||
await this.service.start(); |
||||
else |
||||
await this.service.reload(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
_enable() { |
||||
try { |
||||
const enabled = this.settings.get_boolean('enabled'); |
||||
|
||||
// If the service state matches the enabled setting, we should
|
||||
// toggle the service by toggling the setting
|
||||
if (this.service.active === enabled) |
||||
this.settings.set_boolean('enabled', !enabled); |
||||
|
||||
// Otherwise, we should change the service to match the setting
|
||||
else if (this.service.active) |
||||
this.service.stop(); |
||||
else |
||||
this.service.start(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
_preferences() { |
||||
Gio.Subprocess.new([`${Extension.path}/gsconnect-preferences`], 0); |
||||
} |
||||
|
||||
_sync() { |
||||
const available = this.service.devices.filter(device => { |
||||
return (device.connected && device.paired); |
||||
}); |
||||
const panelMode = this.settings.get_boolean('show-indicators'); |
||||
|
||||
// Hide status indicator if in Panel mode or no devices are available
|
||||
this._indicator.visible = (!panelMode && available.length); |
||||
|
||||
// Show device indicators in Panel mode if available
|
||||
for (const device of this.service.devices) { |
||||
const isAvailable = available.includes(device); |
||||
const indicator = Main.panel.statusArea[device.g_object_path]; |
||||
|
||||
indicator.visible = panelMode && isAvailable; |
||||
|
||||
const menu = this._menus[device.g_object_path]; |
||||
menu.actor.visible = !panelMode && isAvailable; |
||||
menu._title.actor.visible = !panelMode && isAvailable; |
||||
} |
||||
|
||||
// One connected device in User Menu mode
|
||||
if (!panelMode && available.length === 1) { |
||||
const device = available[0]; |
||||
|
||||
// Hide the menu title and move it to the submenu item
|
||||
this._menus[device.g_object_path]._title.actor.visible = false; |
||||
this._item.label.text = device.name; |
||||
|
||||
// Destroy any other device's battery
|
||||
if (this._item._battery && this._item._battery.device !== device) { |
||||
this._item._battery.destroy(); |
||||
this._item._battery = null; |
||||
} |
||||
|
||||
// Add the battery to the submenu item
|
||||
if (!this._item._battery) { |
||||
this._item._battery = new Device.Battery({ |
||||
device: device, |
||||
opacity: 128, |
||||
}); |
||||
this._item.actor.insert_child_below( |
||||
this._item._battery, |
||||
this._item._triangleBin |
||||
); |
||||
} |
||||
} else { |
||||
if (available.length > 1) { |
||||
// TRANSLATORS: %d is the number of devices connected
|
||||
this._item.label.text = Extension.ngettext( |
||||
'%d Connected', |
||||
'%d Connected', |
||||
available.length |
||||
).format(available.length); |
||||
} else { |
||||
this._item.label.text = _('Mobile Devices'); |
||||
} |
||||
|
||||
// Destroy any battery in the submenu item
|
||||
if (this._item._battery) { |
||||
this._item._battery.destroy(); |
||||
this._item._battery = null; |
||||
} |
||||
} |
||||
} |
||||
|
||||
_onDeviceChanged(device, changed, invalidated) { |
||||
try { |
||||
const properties = changed.deepUnpack(); |
||||
|
||||
if (properties.hasOwnProperty('Connected') || |
||||
properties.hasOwnProperty('Paired')) |
||||
this._sync(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
_onDeviceAdded(service, device) { |
||||
try { |
||||
// Device Indicator
|
||||
const indicator = new Device.Indicator({device: device}); |
||||
Main.panel.addToStatusArea(device.g_object_path, indicator); |
||||
|
||||
// Device Menu
|
||||
const menu = new Device.Menu({ |
||||
device: device, |
||||
menu_type: 'list', |
||||
}); |
||||
this._menus[device.g_object_path] = menu; |
||||
this.deviceSection.addMenuItem(menu); |
||||
|
||||
// Device Settings
|
||||
device.settings = new Gio.Settings({ |
||||
settings_schema: Config.GSCHEMA.lookup( |
||||
'org.gnome.Shell.Extensions.GSConnect.Device', |
||||
true |
||||
), |
||||
path: `/org/gnome/shell/extensions/gsconnect/device/${device.id}/`, |
||||
}); |
||||
|
||||
// Keyboard Shortcuts
|
||||
device.__keybindingsChangedId = device.settings.connect( |
||||
'changed::keybindings', |
||||
this._onDeviceKeybindingsChanged.bind(this, device) |
||||
); |
||||
this._onDeviceKeybindingsChanged(device); |
||||
|
||||
// Watch the for status changes
|
||||
device.__deviceChangedId = device.connect( |
||||
'g-properties-changed', |
||||
this._onDeviceChanged.bind(this) |
||||
); |
||||
|
||||
this._sync(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
_onDeviceRemoved(service, device, sync = true) { |
||||
try { |
||||
// Stop watching for status changes
|
||||
if (device.__deviceChangedId) |
||||
device.disconnect(device.__deviceChangedId); |
||||
|
||||
// Release keybindings
|
||||
if (device.__keybindingsChangedId) { |
||||
device.settings.disconnect(device.__keybindingsChangedId); |
||||
device._keybindings.map(id => this._keybindings.remove(id)); |
||||
} |
||||
|
||||
// Destroy the indicator
|
||||
Main.panel.statusArea[device.g_object_path].destroy(); |
||||
|
||||
// Destroy the menu
|
||||
this._menus[device.g_object_path].destroy(); |
||||
delete this._menus[device.g_object_path]; |
||||
|
||||
if (sync) |
||||
this._sync(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
_onDeviceKeybindingsChanged(device) { |
||||
try { |
||||
// Reset any existing keybindings
|
||||
if (device.hasOwnProperty('_keybindings')) |
||||
device._keybindings.map(id => this._keybindings.remove(id)); |
||||
|
||||
device._keybindings = []; |
||||
|
||||
// Get the keybindings
|
||||
const keybindings = device.settings.get_value('keybindings').deepUnpack(); |
||||
|
||||
// Apply the keybindings
|
||||
for (const [action, accelerator] of Object.entries(keybindings)) { |
||||
const [, name, parameter] = Gio.Action.parse_detailed_name(action); |
||||
|
||||
const actionId = this._keybindings.add( |
||||
accelerator, |
||||
() => device.action_group.activate_action(name, parameter) |
||||
); |
||||
|
||||
if (actionId !== 0) |
||||
device._keybindings.push(actionId); |
||||
} |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
async _onEnabledChanged(settings, key) { |
||||
try { |
||||
if (this.settings.get_boolean('enabled')) |
||||
await this.service.start(); |
||||
else |
||||
await this.service.stop(); |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
async _onServiceChanged(service, pspec) { |
||||
try { |
||||
if (this.service.active) { |
||||
// TRANSLATORS: A menu option to deactivate the extension
|
||||
this._enableItem.label.text = _('Turn Off'); |
||||
} else { |
||||
// TRANSLATORS: A menu option to activate the extension
|
||||
this._enableItem.label.text = _('Turn On'); |
||||
|
||||
// If it's enabled, we should try to restart now
|
||||
if (this.settings.get_boolean('enabled')) |
||||
await this.service.start(); |
||||
} |
||||
} catch (e) { |
||||
logError(e, 'GSConnect'); |
||||
} |
||||
} |
||||
|
||||
destroy() { |
||||
// Unhook from Remote.Service
|
||||
if (this.service) { |
||||
this.service.disconnect(this._serviceChangedId); |
||||
this.service.disconnect(this._deviceAddedId); |
||||
this.service.disconnect(this._deviceRemovedId); |
||||
|
||||
for (const device of this.service.devices) |
||||
this._onDeviceRemoved(this.service, device, false); |
||||
|
||||
this.service.destroy(); |
||||
} |
||||
|
||||
// Disconnect any keybindings
|
||||
this._keybindings.destroy(); |
||||
|
||||
// Disconnect from any GSettings changes
|
||||
this.settings.disconnect(this._enabledId); |
||||
this.settings.disconnect(this._panelModeId); |
||||
this.settings.run_dispose(); |
||||
|
||||
// Destroy the PanelMenu.SystemIndicator actors
|
||||
this._item.destroy(); |
||||
this.menu.destroy(); |
||||
|
||||
delete AggregateMenu._gsconnect; |
||||
super.destroy(); |
||||
} |
||||
}); |
||||
|
||||
|
||||
var serviceIndicator = null; |
||||
|
||||
|
||||
function init() { |
||||
// If installed as a user extension, this will install the Desktop entry,
|
||||
// DBus and systemd service files necessary for DBus activation and
|
||||
// GNotifications. Since there's no uninit()/uninstall() hook for extensions
|
||||
// and they're only used *by* GSConnect, they should be okay to leave.
|
||||
Utils.installService(); |
||||
|
||||
// These modify the notification source for GSConnect's GNotifications and
|
||||
// need to be active even when the extension is disabled (eg. lock screen).
|
||||
// Since they *only* affect notifications from GSConnect, it should be okay
|
||||
// to leave them applied.
|
||||
Notification.patchGSConnectNotificationSource(); |
||||
Notification.patchGtkNotificationDaemon(); |
||||
|
||||
// This watches for the service to start and exports a custom clipboard
|
||||
// portal for use on Wayland
|
||||
Clipboard.watchService(); |
||||
} |
||||
|
||||
|
||||
function enable() { |
||||
serviceIndicator = new ServiceIndicator(); |
||||
Notification.patchGtkNotificationSources(); |
||||
} |
||||
|
||||
|
||||
function disable() { |
||||
serviceIndicator.destroy(); |
||||
serviceIndicator = null; |
||||
Notification.unpatchGtkNotificationSources(); |
||||
} |
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env gjs |
||||
|
||||
'use strict'; |
||||
|
||||
imports.gi.versions.Gdk = '3.0'; |
||||
imports.gi.versions.GdkPixbuf = '2.0'; |
||||
imports.gi.versions.Gio = '2.0'; |
||||
imports.gi.versions.GLib = '2.0'; |
||||
imports.gi.versions.GObject = '2.0'; |
||||
imports.gi.versions.Gtk = '3.0'; |
||||
|
||||
const Gdk = imports.gi.Gdk; |
||||
const Gio = imports.gi.Gio; |
||||
const GLib = imports.gi.GLib; |
||||
const GObject = imports.gi.GObject; |
||||
const Gtk = imports.gi.Gtk; |
||||
|
||||
|
||||
// Bootstrap |
||||
function get_datadir() { |
||||
let m = /@(.+):\d+/.exec((new Error()).stack.split('\n')[1]); |
||||
return Gio.File.new_for_path(m[1]).get_parent().get_path(); |
||||
} |
||||
|
||||
imports.searchPath.unshift(get_datadir()); |
||||
imports.config.PACKAGE_DATADIR = imports.searchPath[0]; |
||||
|
||||
|
||||
// Local Imports |
||||
const Config = imports.config; |
||||
const Settings = imports.preferences.service; |
||||
|
||||
|
||||
/** |
||||
* Class representing the GSConnect service daemon. |
||||
*/ |
||||
const Preferences = GObject.registerClass({ |
||||
GTypeName: 'GSConnectPreferences', |
||||
}, class Preferences extends Gtk.Application { |
||||
|
||||
_init() { |
||||
super._init({ |
||||
application_id: 'org.gnome.Shell.Extensions.GSConnect.Preferences', |
||||
resource_base_path: '/org/gnome/Shell/Extensions/GSConnect', |
||||
}); |
||||
|
||||
GLib.set_prgname('gsconnect-preferences'); |
||||
GLib.set_application_name(_('GSConnect Preferences')); |
||||
} |
||||
|
||||
vfunc_activate() { |
||||
if (this._window === undefined) { |
||||
this._window = new Settings.Window({ |
||||
application: this, |
||||
}); |
||||
} |
||||
|
||||
this._window.present(); |
||||
} |
||||
|
||||
vfunc_startup() { |
||||
super.vfunc_startup(); |
||||
|
||||
// Init some resources |
||||
let provider = new Gtk.CssProvider(); |
||||
provider.load_from_resource(`${Config.APP_PATH}/application.css`); |
||||
Gtk.StyleContext.add_provider_for_screen( |
||||
Gdk.Screen.get_default(), |
||||
provider, |
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION |
||||
); |
||||
|
||||
let actions = [ |
||||
['refresh', null], |
||||
['connect', GLib.VariantType.new('s')], |
||||
]; |
||||
|
||||
for (let [name, type] of actions) { |
||||
let action = new Gio.SimpleAction({ |
||||
name: name, |
||||
parameter_type: type, |
||||
}); |
||||
this.add_action(action); |
||||
} |
||||
} |
||||
|
||||
vfunc_activate_action(action_name, parameter) { |
||||
try { |
||||
let paramArray = []; |
||||
|
||||
if (parameter instanceof GLib.Variant) |
||||
paramArray[0] = parameter; |
||||
|
||||
this.get_dbus_connection().call( |
||||
'org.gnome.Shell.Extensions.GSConnect', |
||||
'/org/gnome/Shell/Extensions/GSConnect', |
||||
'org.freedesktop.Application', |
||||
'ActivateAction', |
||||
GLib.Variant.new('(sava{sv})', [action_name, paramArray, {}]), |
||||
null, |
||||
Gio.DBusCallFlags.NONE, |
||||
-1, |
||||
null, |
||||
null |
||||
); |
||||
} catch (e) { |
||||
logError(e); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
(new Preferences()).run([imports.system.programInvocationName].concat(ARGV)); |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,11 @@
|
||||
{ |
||||
"_generated": "Generated by SweetTooth, do not edit", |
||||
"description": "GSConnect is a complete implementation of KDE Connect especially for GNOME Shell with Nautilus, Chrome and Firefox integration. It does not rely on the KDE Connect desktop application and will not work with it installed.\n\nKDE Connect allows devices to securely share content like notifications or files and other features like SMS messaging and remote control. The KDE Connect team has applications for Linux, BSD, Android, Sailfish and Windows.\n\nPlease restart GNOME Shell after updating!\n\nPlease report issues on Github!", |
||||
"name": "GSConnect", |
||||
"shell-version": [ |
||||
"3.38" |
||||
], |
||||
"url": "https://github.com/andyholmes/gnome-shell-extension-gsconnect/wiki", |
||||
"uuid": "gsconnect@andyholmes.github.io", |
||||
"version": 44 |
||||
} |
@ -0,0 +1,183 @@
|
||||
""" |
||||
nautilus-gsconnect.py - A Nautilus extension for sending files via GSConnect. |
||||
|
||||
A great deal of credit and appreciation is owed to the indicator-kdeconnect |
||||
developers for the sister Python script 'kdeconnect-send-nautilus.py': |
||||
|
||||
https://github.com/Bajoja/indicator-kdeconnect/blob/master/data/extensions/kdeconnect-send-nautilus.py |
||||
""" |
||||
|
||||
import gettext |
||||
import os.path |
||||
import sys |
||||
|
||||
import gi |
||||
gi.require_version('Gio', '2.0') |
||||
gi.require_version('GLib', '2.0') |
||||
gi.require_version('GObject', '2.0') |
||||
from gi.repository import Gio, GLib, GObject |
||||
|
||||
|
||||
# Host application detection |
||||
# |
||||
# Nemo seems to reliably identify itself as 'nemo' in argv[0], so we |
||||
# can test for that. Nautilus detection is less reliable, so don't try. |
||||
# See https://github.com/linuxmint/nemo-extensions/issues/330 |
||||
if "nemo" in sys.argv[0].lower(): |
||||
# Host runtime is nemo-python |
||||
gi.require_version('Nemo', '3.0') |
||||
from gi.repository import Nemo as FileManager |
||||
else: |
||||
# Otherwise, just assume it's nautilus-python |
||||
gi.require_version('Nautilus', '3.0') |
||||
from gi.repository import Nautilus as FileManager |
||||
|
||||
|
||||
SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect' |
||||
SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect' |
||||
|
||||
# Init gettext translations |
||||
LOCALE_DIR = os.path.join(GLib.get_user_data_dir(), |
||||
'gnome-shell', 'extensions', |
||||
'gsconnect@andyholmes.github.io', 'locale') |
||||
|
||||
if not os.path.exists(LOCALE_DIR): |
||||
LOCALE_DIR = None |
||||
|
||||
try: |
||||
i18n = gettext.translation(SERVICE_NAME, |
||||
localedir=LOCALE_DIR) |
||||
_ = i18n.gettext |
||||
|
||||
except (IOError, OSError) as e: |
||||
print('GSConnect: {0}'.format(e.strerror)) |
||||
i18n = gettext.translation(SERVICE_NAME, |
||||
localedir=LOCALE_DIR, |
||||
fallback=True) |
||||
_ = i18n.gettext |
||||
|
||||
|
||||
class GSConnectShareExtension(GObject.Object, FileManager.MenuProvider): |
||||
"""A context menu for sending files via GSConnect.""" |
||||
|
||||
def __init__(self): |
||||
"""Initialize the DBus ObjectManager""" |
||||
|
||||
GObject.Object.__init__(self) |
||||
|
||||
self.devices = {} |
||||
|
||||
Gio.DBusProxy.new_for_bus(Gio.BusType.SESSION, |
||||
Gio.DBusProxyFlags.DO_NOT_AUTO_START, |
||||
None, |
||||
SERVICE_NAME, |
||||
SERVICE_PATH, |
||||
'org.freedesktop.DBus.ObjectManager', |
||||
None, |
||||
self._init_async, |
||||
None) |
||||
|
||||
def _init_async(self, proxy, res, user_data): |
||||
proxy = proxy.new_for_bus_finish(res) |
||||
proxy.connect('notify::g-name-owner', self._on_name_owner_changed) |
||||
proxy.connect('g-signal', self._on_g_signal) |
||||
|
||||
self._on_name_owner_changed(proxy, None) |
||||
|
||||
def _on_g_signal(self, proxy, sender_name, signal_name, parameters): |
||||
# Wait until the service is ready |
||||
if proxy.props.g_name_owner is None: |
||||
return |
||||
|
||||
objects = parameters.unpack() |
||||
|
||||
if signal_name == 'InterfacesAdded': |
||||
for object_path, props in objects.items(): |
||||
props = props['org.gnome.Shell.Extensions.GSConnect.Device'] |
||||
|
||||
self.devices[object_path] = (props['Name'], |
||||
Gio.DBusActionGroup.get( |
||||
proxy.get_connection(), |
||||
SERVICE_NAME, |
||||
object_path)) |
||||
elif signal_name == 'InterfacesRemoved': |
||||
for object_path in objects: |
||||
try: |
||||
del self.devices[object_path] |
||||
except KeyError: |
||||
pass |
||||
|
||||
def _on_name_owner_changed(self, proxy, pspec): |
||||
# Wait until the service is ready |
||||
if proxy.props.g_name_owner is None: |
||||
self.devices = {} |
||||
else: |
||||
proxy.call('GetManagedObjects', |
||||
None, |
||||
Gio.DBusCallFlags.NO_AUTO_START, |
||||
-1, |
||||
None, |
||||
self._get_managed_objects, |
||||
None) |
||||
|
||||
def _get_managed_objects(self, proxy, res, user_data): |
||||
objects = proxy.call_finish(res)[0] |
||||
|
||||
for object_path, props in objects.items(): |
||||
props = props['org.gnome.Shell.Extensions.GSConnect.Device'] |
||||
|
||||
self.devices[object_path] = (props['Name'], |
||||
Gio.DBusActionGroup.get( |
||||
proxy.get_connection(), |
||||
SERVICE_NAME, |
||||
object_path)) |
||||
|
||||
def send_files(self, menu, files, action_group): |
||||
"""Send *files* to *device_id*""" |
||||
|
||||
for file in files: |
||||
variant = GLib.Variant('(sb)', (file.get_uri(), False)) |
||||
action_group.activate_action('shareFile', variant) |
||||
|
||||
def get_file_items(self, window, files): |
||||
"""Return a list of select files to be sent""" |
||||
|
||||
# Only accept regular files |
||||
for uri in files: |
||||
if uri.get_uri_scheme() != 'file' or uri.is_directory(): |
||||
return () |
||||
|
||||
# Enumerate capable devices |
||||
devices = [] |
||||
|
||||
for name, action_group in self.devices.values(): |
||||
if action_group.get_action_enabled('shareFile'): |
||||
devices.append([name, action_group]) |
||||
|
||||
# No capable devices; don't show menu entry |
||||
if not devices: |
||||
return () |
||||
|
||||
# Context Menu Item |
||||
menu = FileManager.MenuItem( |
||||
name='GSConnectShareExtension::Devices', |
||||
label=_('Send To Mobile Device') |
||||
) |
||||
|
||||
# Context Submenu |
||||
submenu = FileManager.Menu() |
||||
menu.set_submenu(submenu) |
||||
|
||||
# Context Submenu Items |
||||
for name, action_group in devices: |
||||
item = FileManager.MenuItem( |
||||
name='GSConnectShareExtension::Device' + name, |
||||
label=name |
||||
) |
||||
|
||||
item.connect('activate', self.send_files, files, action_group) |
||||
|
||||
submenu.append_item(item) |
||||
|
||||
return (menu,) |
||||
|
Binary file not shown.
@ -0,0 +1,41 @@
|
||||
'use strict'; |
||||
|
||||
const Gettext = imports.gettext; |
||||
|
||||
const Gio = imports.gi.Gio; |
||||
const GLib = imports.gi.GLib; |
||||
|
||||
const Config = imports.config; |
||||
|
||||
|
||||
// Ensure config.js is setup properly
|
||||
const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']); |
||||
|
||||
if (Config.PACKAGE_DATADIR.startsWith(userDir)) { |
||||
Config.IS_USER = true; |
||||
|
||||
Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`; |
||||
Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`; |
||||
} |
||||
|
||||
|
||||
// Init Gettext
|
||||
String.prototype.format = imports.format.format; |
||||
Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR); |
||||
globalThis._ = GLib.dgettext.bind(null, Config.APP_ID); |
||||
globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID); |
||||
|
||||
|
||||
// Init GResources
|
||||
Gio.Resource.load( |
||||
GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`]) |
||||
)._register(); |
||||
|
||||
|
||||
// Init GSchema
|
||||
Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory( |
||||
Config.GSETTINGS_SCHEMA_DIR, |
||||
Gio.SettingsSchemaSource.get_default(), |
||||
false |
||||
); |
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,312 @@
|
||||
'use strict'; |
||||
|
||||
const Gdk = imports.gi.Gdk; |
||||
const Gio = imports.gi.Gio; |
||||
const GLib = imports.gi.GLib; |
||||
const GObject = imports.gi.GObject; |
||||
const Gtk = imports.gi.Gtk; |
||||
|
||||
|
||||
/* |
||||
* A list of modifier keysyms we ignore |
||||
*/ |
||||
const _MODIFIERS = [ |
||||
Gdk.KEY_Alt_L, |
||||
Gdk.KEY_Alt_R, |
||||
Gdk.KEY_Caps_Lock, |
||||
Gdk.KEY_Control_L, |
||||
Gdk.KEY_Control_R, |
||||
Gdk.KEY_Meta_L, |
||||
Gdk.KEY_Meta_R, |
||||
Gdk.KEY_Num_Lock, |
||||
Gdk.KEY_Shift_L, |
||||
Gdk.KEY_Shift_R, |
||||
Gdk.KEY_Super_L, |
||||
Gdk.KEY_Super_R, |
||||
]; |
||||
|
||||
/** |
||||
* Response enum for ShortcutChooserDialog |
||||
*/ |
||||
var ResponseType = { |
||||
CANCEL: Gtk.ResponseType.CANCEL, |
||||
SET: Gtk.ResponseType.APPLY, |
||||
UNSET: 2, |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* A simplified version of the shortcut editor from GNOME Control Center |
||||
*/ |
||||
var ShortcutChooserDialog = GObject.registerClass({ |
||||
GTypeName: 'GSConnectPreferencesShortcutEditor', |
||||
Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-shortcut-editor.ui', |
||||
Children: [ |
||||
'cancel-button', 'set-button', |
||||
'stack', 'summary-label', |
||||
'shortcut-label', 'conflict-label', |
||||
], |
||||
}, class ShortcutChooserDialog extends Gtk.Dialog { |
||||
|
||||
_init(params) { |
||||
super._init({ |
||||
transient_for: Gio.Application.get_default().get_active_window(), |
||||
use_header_bar: true, |
||||
}); |
||||
|
||||
this._seat = Gdk.Display.get_default().get_default_seat(); |
||||
|
||||
// Current accelerator or %null
|
||||
this.accelerator = params.accelerator; |
||||
|
||||
// TRANSLATORS: Summary of a keyboard shortcut function
|
||||
// Example: Enter a new shortcut to change Messaging
|
||||
this.summary = _('Enter a new shortcut to change <b>%s</b>').format( |
||||
params.summary |
||||
); |
||||
} |
||||
|
||||
get accelerator() { |
||||
return this.shortcut_label.accelerator; |
||||
} |
||||
|
||||
set accelerator(value) { |
||||
this.shortcut_label.accelerator = value; |
||||
} |
||||
|
||||
get summary() { |
||||
return this.summary_label.label; |
||||
} |
||||
|
||||
set summary(value) { |
||||
this.summary_label.label = value; |
||||
} |
||||
|
||||
vfunc_key_press_event(event) { |
||||
let keyvalLower = Gdk.keyval_to_lower(event.keyval); |
||||
let realMask = event.state & Gtk.accelerator_get_default_mod_mask(); |
||||
|
||||
// TODO: Critical: 'WIDGET_REALIZED_FOR_EVENT (widget, event)' failed
|
||||
if (_MODIFIERS.includes(keyvalLower)) |
||||
return true; |
||||
|
||||
// Normalize Tab
|
||||
if (keyvalLower === Gdk.KEY_ISO_Left_Tab) |
||||
keyvalLower = Gdk.KEY_Tab; |
||||
|
||||
// Put shift back if it changed the case of the key, not otherwise.
|
||||
if (keyvalLower !== event.keyval) |
||||
realMask |= Gdk.ModifierType.SHIFT_MASK; |
||||
|
||||
// HACK: we don't want to use SysRq as a keybinding (but we do want
|
||||
// Alt+Print), so we avoid translation from Alt+Print to SysRq
|
||||
if (keyvalLower === Gdk.KEY_Sys_Req && (realMask & Gdk.ModifierType.MOD1_MASK) !== 0) |
||||
keyvalLower = Gdk.KEY_Print; |
||||
|
||||
// A single Escape press cancels the editing
|
||||
if (realMask === 0 && keyvalLower === Gdk.KEY_Escape) { |
||||
this.response(ResponseType.CANCEL); |
||||
return false; |
||||
} |
||||
|
||||
// Backspace disables the current shortcut
|
||||
if (realMask === 0 && keyvalLower === Gdk.KEY_BackSpace) { |
||||
this.response(ResponseType.UNSET); |
||||
return false; |
||||
} |
||||
|
||||
// CapsLock isn't supported as a keybinding modifier, so keep it from
|
||||
// confusing us
|
||||
realMask &= ~Gdk.ModifierType.LOCK_MASK; |
||||
|
||||
if (keyvalLower !== 0 && realMask !== 0) { |
||||
this._ungrab(); |
||||
|
||||
// Set the accelerator property/label
|
||||
this.accelerator = Gtk.accelerator_name(keyvalLower, realMask); |
||||
|
||||
// TRANSLATORS: When a keyboard shortcut is unavailable
|
||||
// Example: [Ctrl]+[S] is already being used
|
||||
this.conflict_label.label = _('%s is already being used').format( |
||||
Gtk.accelerator_get_label(keyvalLower, realMask) |
||||
); |
||||
|
||||
// Show Cancel button and switch to confirm/conflict page
|
||||
this.cancel_button.visible = true; |
||||
this.stack.visible_child_name = 'confirm'; |
||||
|
||||
this._check(); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
async _check() { |
||||
try { |
||||
const available = await checkAccelerator(this.accelerator); |
||||
this.set_button.visible = available; |
||||
this.conflict_label.visible = !available; |
||||
} catch (e) { |
||||
logError(e); |
||||
this.response(ResponseType.CANCEL); |
||||
} |
||||
} |
||||
|
||||
_grab() { |
||||
const success = this._seat.grab( |
||||
this.get_window(), |
||||
Gdk.SeatCapabilities.KEYBOARD, |
||||
true, // owner_events
|
||||
null, // cursor
|
||||
null, // event
|
||||
null |
||||
); |
||||
|
||||
if (success !== Gdk.GrabStatus.SUCCESS) |
||||
return this.response(ResponseType.CANCEL); |
||||
|
||||
if (!this._seat.get_keyboard() && !this._seat.get_pointer()) |
||||
return this.response(ResponseType.CANCEL); |
||||
|
||||
this.grab_add(); |
||||
} |
||||
|
||||
_ungrab() { |
||||
this._seat.ungrab(); |
||||
this.grab_remove(); |
||||
} |
||||
|
||||
// Override to use our own ungrab process
|
||||
response(response_id) { |
||||
this.hide(); |
||||
this._ungrab(); |
||||
|
||||
return super.response(response_id); |
||||
} |
||||
|
||||
// Override with a non-blocking version of Gtk.Dialog.run()
|
||||
run() { |
||||
this.show(); |
||||
|
||||
// Wait a bit before attempting grab
|
||||
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { |
||||
this._grab(); |
||||
return GLib.SOURCE_REMOVE; |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
|
||||
/** |
||||
* Check the availability of an accelerator using GNOME Shell's DBus interface. |
||||
* |
||||
* @param {string} accelerator - An accelerator |
||||
* @param {number} [modeFlags] - Mode Flags |
||||
* @param {number} [grabFlags] - Grab Flags |
||||
* @param {boolean} %true if available, %false on error or unavailable |
||||
*/ |
||||
async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) { |
||||
try { |
||||
let result = false; |
||||
|
||||
// Try to grab the accelerator
|
||||
const action = await new Promise((resolve, reject) => { |
||||
Gio.DBus.session.call( |
||||
'org.gnome.Shell', |
||||
'/org/gnome/Shell', |
||||
'org.gnome.Shell', |
||||
'GrabAccelerator', |
||||
new GLib.Variant('(suu)', [accelerator, modeFlags, grabFlags]), |
||||
null, |
||||
Gio.DBusCallFlags.NONE, |
||||
-1, |
||||
null, |
||||
(connection, res) => { |
||||
try { |
||||
res = connection.call_finish(res); |
||||
resolve(res.deepUnpack()[0]); |
||||
} catch (e) { |
||||
reject(e); |
||||
} |
||||
} |
||||
); |
||||
}); |
||||
|
||||
// If successful, use the result of ungrabbing as our return
|
||||
if (action !== 0) { |
||||
result = await new Promise((resolve, reject) => { |
||||
Gio.DBus.session.call( |
||||
'org.gnome.Shell', |
||||
'/org/gnome/Shell', |
||||
'org.gnome.Shell', |
||||
'UngrabAccelerator', |
||||
new GLib.Variant('(u)', [action]), |
||||
null, |
||||
Gio.DBusCallFlags.NONE, |
||||
-1, |
||||
null, |
||||
(connection, res) => { |
||||
try { |
||||
res = connection.call_finish(res); |
||||
resolve(res.deepUnpack()[0]); |
||||
} catch (e) { |
||||
reject(e); |
||||
} |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
return result; |
||||
} catch (e) { |
||||
logError(e); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Show a dialog to get a keyboard shortcut from a user. |
||||
* |
||||
* @param {string} summary - A description of the keybinding's function |
||||
* @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel |
||||
* @return {string} An accelerator or %null if it should be unset. |
||||
*/ |
||||
async function getAccelerator(summary, accelerator = null) { |
||||
try { |
||||
const dialog = new ShortcutChooserDialog({ |
||||
summary: summary, |
||||
accelerator: accelerator, |
||||
}); |
||||
|
||||
accelerator = await new Promise((resolve, reject) => { |
||||
dialog.connect('response', (dialog, response) => { |
||||
switch (response) { |
||||
case ResponseType.SET: |
||||
accelerator = dialog.accelerator; |
||||
break; |
||||
|
||||
case ResponseType.UNSET: |
||||
accelerator = null; |
||||
break; |
||||
|
||||
case ResponseType.CANCEL: |
||||
// leave the accelerator as passed in
|
||||
break; |
||||
} |
||||
|
||||
dialog.destroy(); |
||||
|
||||
resolve(accelerator); |
||||
}); |
||||
|
||||
dialog.run(); |
||||
}); |
||||
|
||||
return accelerator; |
||||
} catch (e) { |
||||
logError(e); |
||||
return accelerator; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,657 @@
|
||||
'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; |
||||
} |
||||
}); |
||||
|
||||
|
||||
function rowSeparators(row, before) { |
||||
const header = row.get_header(); |
||||
|
||||
if (before === null) { |
||||
if (header !== null) |
||||
header.destroy(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (header === null) |
||||
row.set_header(new Gtk.Separator({visible: true})); |
||||
} |
||||
|
||||
|
||||
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) |
||||
< |