service.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. 'use strict';
  5. const GLib = imports.gi.GLib;
  6. const Gio = imports.gi.Gio;
  7. const GObject = imports.gi.GObject;
  8. const Gtk = imports.gi.Gtk;
  9. const Config = imports.config;
  10. /*
  11. * Issue Header
  12. */
  13. const ISSUE_HEADER = `
  14. GSConnect: ${Config.PACKAGE_VERSION} (${Config.IS_USER ? 'user' : 'system'})
  15. GJS: ${imports.system.version}
  16. Session: ${GLib.getenv('XDG_SESSION_TYPE')}
  17. OS: ${GLib.get_os_info('PRETTY_NAME')}
  18. `;
  19. /**
  20. * A dialog for selecting a device
  21. */
  22. var DeviceChooser = GObject.registerClass({
  23. GTypeName: 'GSConnectServiceDeviceChooser',
  24. Properties: {
  25. 'action-name': GObject.ParamSpec.string(
  26. 'action-name',
  27. 'Action Name',
  28. 'The name of the associated action, like "sendFile"',
  29. GObject.ParamFlags.READWRITE,
  30. null
  31. ),
  32. 'action-target': GObject.param_spec_variant(
  33. 'action-target',
  34. 'Action Target',
  35. 'The parameter for action invocations',
  36. new GLib.VariantType('*'),
  37. null,
  38. GObject.ParamFlags.READWRITE
  39. ),
  40. },
  41. Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-device-chooser.ui',
  42. Children: ['device-list', 'cancel-button', 'select-button'],
  43. }, class DeviceChooser extends Gtk.Dialog {
  44. _init(params = {}) {
  45. super._init({
  46. use_header_bar: true,
  47. application: Gio.Application.get_default(),
  48. });
  49. this.set_keep_above(true);
  50. // HeaderBar
  51. this.get_header_bar().subtitle = params.title;
  52. // Dialog Action
  53. this.action_name = params.action_name;
  54. this.action_target = params.action_target;
  55. // Device List
  56. this.device_list.set_sort_func(this._sortDevices);
  57. this._devicesChangedId = this.application.settings.connect(
  58. 'changed::devices',
  59. this._onDevicesChanged.bind(this)
  60. );
  61. this._onDevicesChanged();
  62. }
  63. vfunc_response(response_id) {
  64. if (response_id === Gtk.ResponseType.OK) {
  65. try {
  66. const device = this.device_list.get_selected_row().device;
  67. device.activate_action(this.action_name, this.action_target);
  68. } catch (e) {
  69. logError(e);
  70. }
  71. }
  72. this.destroy();
  73. }
  74. get action_name() {
  75. if (this._action_name === undefined)
  76. this._action_name = null;
  77. return this._action_name;
  78. }
  79. set action_name(name) {
  80. this._action_name = name;
  81. }
  82. get action_target() {
  83. if (this._action_target === undefined)
  84. this._action_target = null;
  85. return this._action_target;
  86. }
  87. set action_target(variant) {
  88. this._action_target = variant;
  89. }
  90. _onDeviceActivated(box, row) {
  91. this.response(Gtk.ResponseType.OK);
  92. }
  93. _onDeviceSelected(box) {
  94. this.set_response_sensitive(
  95. Gtk.ResponseType.OK,
  96. (box.get_selected_row())
  97. );
  98. }
  99. _onDevicesChanged() {
  100. // Collect known devices
  101. const devices = {};
  102. for (const [id, device] of this.application.manager.devices.entries())
  103. devices[id] = device;
  104. // Prune device rows
  105. this.device_list.foreach(row => {
  106. if (!devices.hasOwnProperty(row.name))
  107. row.destroy();
  108. else
  109. delete devices[row.name];
  110. });
  111. // Add new devices
  112. for (const device of Object.values(devices)) {
  113. const action = device.lookup_action(this.action_name);
  114. if (action === null)
  115. continue;
  116. const row = new Gtk.ListBoxRow({
  117. visible: action.enabled,
  118. });
  119. row.set_name(device.id);
  120. row.device = device;
  121. action.bind_property(
  122. 'enabled',
  123. row,
  124. 'visible',
  125. Gio.SettingsBindFlags.DEFAULT
  126. );
  127. const grid = new Gtk.Grid({
  128. column_spacing: 12,
  129. margin: 6,
  130. visible: true,
  131. });
  132. row.add(grid);
  133. const icon = new Gtk.Image({
  134. icon_name: device.icon_name,
  135. pixel_size: 32,
  136. visible: true,
  137. });
  138. grid.attach(icon, 0, 0, 1, 1);
  139. const name = new Gtk.Label({
  140. label: device.name,
  141. halign: Gtk.Align.START,
  142. hexpand: true,
  143. visible: true,
  144. });
  145. grid.attach(name, 1, 0, 1, 1);
  146. this.device_list.add(row);
  147. }
  148. if (this.device_list.get_selected_row() === null)
  149. this.device_list.select_row(this.device_list.get_row_at_index(0));
  150. }
  151. _sortDevices(row1, row2) {
  152. return row1.device.name.localeCompare(row2.device.name);
  153. }
  154. });
  155. /**
  156. * A dialog for reporting an error.
  157. */
  158. var ErrorDialog = GObject.registerClass({
  159. GTypeName: 'GSConnectServiceErrorDialog',
  160. Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-error-dialog.ui',
  161. Children: [
  162. 'error-stack',
  163. 'expander-arrow',
  164. 'gesture',
  165. 'report-button',
  166. 'revealer',
  167. ],
  168. }, class ErrorDialog extends Gtk.Window {
  169. _init(error) {
  170. super._init({
  171. application: Gio.Application.get_default(),
  172. title: `GSConnect: ${error.name}`,
  173. });
  174. this.set_keep_above(true);
  175. this.error = error;
  176. this.error_stack.buffer.text = `${error.message}\n\n${error.stack}`;
  177. this.gesture.connect('released', this._onReleased.bind(this));
  178. }
  179. _onClicked(button) {
  180. if (this.report_button === button) {
  181. const uri = this._buildUri(this.error.message, this.error.stack);
  182. Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
  183. }
  184. this.destroy();
  185. }
  186. _onReleased(gesture, n_press) {
  187. if (n_press === 1)
  188. this.revealer.reveal_child = !this.revealer.reveal_child;
  189. }
  190. _onRevealChild(revealer, pspec) {
  191. this.expander_arrow.icon_name = this.revealer.reveal_child
  192. ? 'pan-down-symbolic'
  193. : 'pan-end-symbolic';
  194. }
  195. _buildUri(message, stack) {
  196. const body = `\`\`\`${ISSUE_HEADER}\n${stack}\n\`\`\``;
  197. const titleQuery = encodeURIComponent(message).replace('%20', '+');
  198. const bodyQuery = encodeURIComponent(body).replace('%20', '+');
  199. const uri = `${Config.PACKAGE_BUGREPORT}?title=${titleQuery}&body=${bodyQuery}`;
  200. // Reasonable URI length limit
  201. if (uri.length > 2000)
  202. return uri.substr(0, 2000);
  203. return uri;
  204. }
  205. });