From 6d45aaa0425ecc9e9277b14a5757bf4712ed2ede Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Wed, 17 Jan 2024 15:38:16 -0500 Subject: [PATCH] Add base gnome extensions --- .../BWClipboard.js | 34 + .../BingWallpaper@ineffable-gmail.com/LICENSE | 675 +++++++ .../README.md | 99 ++ .../BingWallpaper@ineffable-gmail.com/blur.js | 159 ++ .../carousel.js | 228 +++ .../extension.js | 1104 ++++++++++++ .../icons/bing-symbolic.svg | 81 + .../icons/brick-symbolic.svg | 117 ++ .../icons/checked-symbolic.svg | 1 + .../icons/fav-symbolic.svg | 1 + .../icons/game-die-symbolic.svg | 11 + .../icons/high-frame-symbolic.svg | 67 + .../icons/low-frame-symbolic.svg | 67 + .../icons/mid-frame-symbolic.svg | 67 + .../icons/trash-empty-symbolic.svg | 28 + .../icons/trash-full-symbolic.svg | 62 + .../icons/unchecked-symbolic.svg | 1 + .../icons/unfav-symbolic.svg | 1 + .../locale/ar/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5686 bytes .../locale/ca/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2862 bytes .../locale/cs/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 7358 bytes .../locale/de/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 7377 bytes .../locale/en_AU/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5192 bytes .../locale/es/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5226 bytes .../locale/es_ES/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5159 bytes .../locale/fi_FI/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 4884 bytes .../locale/fr/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2849 bytes .../locale/fr_FR/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 4792 bytes .../locale/hu/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2904 bytes .../locale/id/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2722 bytes .../locale/it/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 7186 bytes .../locale/ja/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5562 bytes .../locale/ko/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5758 bytes .../locale/nb/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2659 bytes .../locale/nl/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2429 bytes .../locale/nn/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2775 bytes .../locale/pl/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2632 bytes .../locale/pt/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 6216 bytes .../locale/pt_BR/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5809 bytes .../locale/ru/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 3047 bytes .../locale/ru_RU/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 9278 bytes .../locale/sv/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5873 bytes .../locale/tr/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 7556 bytes .../locale/uk/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 9311 bytes .../locale/zh_CN/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 5184 bytes .../locale/zh_TW/LC_MESSAGES/BingWallpaper.mo | Bin 0 -> 2654 bytes .../metadata.json | 13 + .../prefs.js | 247 +++ .../schemas/gschemas.compiled | Bin 0 -> 2432 bytes ...shell.extensions.bingwallpaper.gschema.xml | 249 +++ .../stylesheet.css | 2 + .../thumbnail.js | 29 + .../ui/Settings4.ui | 1451 +++++++++++++++ .../ui/carousel4.ui | 209 +++ .../ui/prefs.css | 4 + .../utils.js | 623 +++++++ .../extensions/Vitals@CoreCoding.com/LICENSE | 339 ++++ .../Vitals@CoreCoding.com/extension.js | 577 ++++++ .../Vitals@CoreCoding.com/helpers/file.js | 82 + .../icons/battery-symbolic.svg | 8 + .../icons/cpu-symbolic.svg | 1 + .../icons/fan-symbolic.svg | 133 ++ .../icons/memory-symbolic.svg | 1 + .../icons/network-download-symbolic.svg | 1 + .../icons/network-symbolic.svg | 1 + .../icons/network-upload-symbolic.svg | 1 + .../icons/storage-symbolic.svg | 3 + .../icons/system-symbolic.svg | 3 + .../icons/temperature-symbolic.svg | 45 + .../icons/voltage-symbolic.svg | 125 ++ .../locale/ca/LC_MESSAGES/vitals.mo | Bin 0 -> 3343 bytes .../locale/cs/LC_MESSAGES/vitals.mo | Bin 0 -> 8345 bytes .../locale/de/LC_MESSAGES/vitals.mo | Bin 0 -> 8159 bytes .../locale/es/LC_MESSAGES/vitals.mo | Bin 0 -> 3298 bytes .../locale/fi_FI/LC_MESSAGES/vitals.mo | Bin 0 -> 4004 bytes .../locale/fr/LC_MESSAGES/vitals.mo | Bin 0 -> 7288 bytes .../locale/it/LC_MESSAGES/vitals.mo | Bin 0 -> 3030 bytes .../locale/nl/LC_MESSAGES/vitals.mo | Bin 0 -> 6939 bytes .../locale/oc/LC_MESSAGES/vitals.mo | Bin 0 -> 3817 bytes .../locale/pl/LC_MESSAGES/vitals.mo | Bin 0 -> 3501 bytes .../locale/pt/LC_MESSAGES/vitals.mo | Bin 0 -> 3935 bytes .../locale/pt_BR/LC_MESSAGES/vitals.mo | Bin 0 -> 7407 bytes .../locale/ru/LC_MESSAGES/vitals.mo | Bin 0 -> 10711 bytes .../locale/sk/LC_MESSAGES/vitals.mo | Bin 0 -> 8559 bytes .../locale/tr/LC_MESSAGES/vitals.mo | Bin 0 -> 8416 bytes .../locale/uk/LC_MESSAGES/vitals.mo | Bin 0 -> 10967 bytes .../locale/zh_CN/LC_MESSAGES/vitals.mo | Bin 0 -> 7900 bytes .../Vitals@CoreCoding.com/menuItem.js | 77 + .../Vitals@CoreCoding.com/metadata.json | 13 + .../extensions/Vitals@CoreCoding.com/prefs.js | 137 ++ .../extensions/Vitals@CoreCoding.com/prefs.ui | 1211 +++++++++++++ .../schemas/gschemas.compiled | Bin 0 -> 1780 bytes ....gnome.shell.extensions.vitals.gschema.xml | 140 ++ .../Vitals@CoreCoding.com/sensors.js | 671 +++++++ .../Vitals@CoreCoding.com/stylesheet.css | 16 + .../Vitals@CoreCoding.com/values.js | 337 ++++ .../appIndicator.js | 1571 +++++++++++++++++ .../dbusMenu.js | 950 ++++++++++ .../dbusProxy.js | 103 ++ .../extension.js | 89 + .../iconCache.js | 179 ++ .../indicatorStatusIcon.js | 592 +++++++ .../interfaces-xml/DBusMenu.xml | 66 + .../interfaces-xml/StatusNotifierItem.xml | 130 ++ .../interfaces-xml/StatusNotifierWatcher.xml | 38 + .../interfaces.js | 52 + .../cs/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2225 bytes .../de/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 949 bytes .../fr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2377 bytes .../hu/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2434 bytes .../it/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1039 bytes .../ja/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 972 bytes .../ko/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2373 bytes .../nl/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1024 bytes .../oc/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2334 bytes .../pl/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2451 bytes .../LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1067 bytes .../ru/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 3067 bytes .../sk/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2239 bytes .../sr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1132 bytes .../LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 1020 bytes .../ta/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 3523 bytes .../tr/LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2497 bytes .../LC_MESSAGES/AppIndicatorExtension.mo | Bin 0 -> 2338 bytes .../metadata.json | 13 + .../pixmapsUtils.js | 68 + .../prefs.js | 326 ++++ .../promiseUtils.js | 324 ++++ .../schemas/gschemas.compiled | Bin 0 -> 820 bytes ....shell.extensions.appindicator.gschema.xml | 49 + .../settingsManager.js | 55 + .../statusNotifierWatcher.js | 287 +++ .../trayIconsManager.js | 104 ++ .../util.js | 434 +++++ .../autoActivities.js | 228 +++ .../extension.js | 36 + .../metadata.json | 19 + .../prefs.js | 119 ++ .../prefs.ui | 124 ++ .../schemas/gschemas.compiled | Bin 0 -> 708 bytes ...ell.extensions.auto-activities.gschema.xml | 29 + .../components/appfolders.js | 261 +++ .../components/applications.js | 548 ++++++ .../components/dash_to_dock.js | 322 ++++ .../components/lockscreen.js | 167 ++ .../components/overview.js | 309 ++++ .../blur-my-shell@aunetx/components/panel.js | 695 ++++++++ .../components/screenshot.js | 173 ++ .../components/window_list.js | 162 ++ .../conveniences/connections.js | 101 ++ .../conveniences/effects_manager.js | 89 + .../blur-my-shell@aunetx/conveniences/keys.js | 131 ++ .../conveniences/settings.js | 182 ++ .../blur-my-shell@aunetx/dbus/client.js | 58 + .../blur-my-shell@aunetx/dbus/iface.xml | 12 + .../blur-my-shell@aunetx/dbus/services.js | 90 + .../effects/color_effect.glsl | 13 + .../effects/color_effect.js | 181 ++ .../effects/noise_effect.glsl | 20 + .../effects/noise_effect.js | 109 ++ .../effects/paint_signals.js | 89 + .../blur-my-shell@aunetx/extension.js | 641 +++++++ .../scalable/actions/add-window-symbolic.svg | 4 + .../actions/applications-symbolic.svg | 56 + .../actions/bottom-panel-symbolic.svg | 44 + .../scalable/actions/dash-symbolic.svg | 44 + .../scalable/actions/general-symbolic.svg | 64 + .../actions/heart-filled-symbolic.svg | 40 + .../scalable/actions/other-symbolic.svg | 1 + .../scalable/actions/overview-symbolic.svg | 56 + .../actions/remove-window-symbolic.svg | 3 + .../scalable/actions/reset-symbolic.svg | 7 + .../scalable/actions/select-mode-symbolic.svg | 2 + .../actions/select-window-symbolic.svg | 2 + .../locale/ar/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 12893 bytes .../locale/az/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 1089 bytes .../locale/bg/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 4362 bytes .../locale/ca/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 1402 bytes .../locale/cs/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11360 bytes .../locale/de/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11858 bytes .../locale/el/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 15185 bytes .../locale/es/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11744 bytes .../locale/fr/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11908 bytes .../locale/he/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 661 bytes .../locale/hu/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11464 bytes .../locale/it/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11739 bytes .../locale/ka/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 3077 bytes .../locale/ko/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 4499 bytes .../locale/nb_NO/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 3908 bytes .../locale/nl/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11469 bytes .../locale/nn/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 1639 bytes .../locale/pl/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11370 bytes .../locale/pt/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11087 bytes .../locale/pt_BR/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11531 bytes .../locale/ru/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 14926 bytes .../locale/sl/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11134 bytes .../locale/sv/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11310 bytes .../locale/ta/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 19778 bytes .../locale/tr/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 11767 bytes .../locale/uk/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 14402 bytes .../locale/vi/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 7854 bytes .../zh_Hans/LC_MESSAGES/blur-my-shell.mo | Bin 0 -> 10394 bytes .../blur-my-shell@aunetx/metadata.json | 20 + .../preferences/applications.js | 174 ++ .../preferences/customize_row.js | 163 ++ .../blur-my-shell@aunetx/preferences/dash.js | 43 + .../preferences/general.js | 49 + .../blur-my-shell@aunetx/preferences/menu.js | 67 + .../blur-my-shell@aunetx/preferences/other.js | 49 + .../preferences/overview.js | 47 + .../blur-my-shell@aunetx/preferences/panel.js | 65 + .../preferences/window_row.js | 109 ++ .../extensions/blur-my-shell@aunetx/prefs.js | 41 + .../schemas/gschemas.compiled | Bin 0 -> 8160 bytes ...shell.extensions.blur-my-shell.gschema.xml | 467 +++++ .../blur-my-shell@aunetx/stylesheet.css | 334 ++++ .../blur-my-shell@aunetx/ui/applications.ui | 163 ++ .../blur-my-shell@aunetx/ui/customize-row.ui | 143 ++ .../blur-my-shell@aunetx/ui/dash.ui | 73 + .../blur-my-shell@aunetx/ui/general.ui | 234 +++ .../blur-my-shell@aunetx/ui/menu.ui | 37 + .../blur-my-shell@aunetx/ui/other.ui | 62 + .../blur-my-shell@aunetx/ui/overview.ui | 94 + .../blur-my-shell@aunetx/ui/panel.ui | 141 ++ .../blur-my-shell@aunetx/ui/window-row.ui | 43 + .../gsconnect@andyholmes.github.io/config.js | 22 + .../gsconnect@andyholmes.github.io/config.mjs | 12 + .../extension.js | 407 +++++ .../gsconnect-preferences | 127 ++ .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 20859 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 21187 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14247 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18638 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 12438 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18366 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 23921 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18586 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15903 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 21364 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 17281 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18747 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 13943 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14325 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18934 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 11530 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 17949 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16896 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18231 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16332 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 7731 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 19299 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 18505 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16871 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 22736 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 17143 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 8802 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 10784 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 16752 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 15574 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 23303 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 14997 bytes .../org.gnome.Shell.Extensions.GSConnect.mo | Bin 0 -> 10769 bytes .../metadata.json | 11 + .../nautilus-gsconnect.py | 200 +++ ...gnome.Shell.Extensions.GSConnect.gresource | Bin 0 -> 65132 bytes .../preferences/device.js | 1112 ++++++++++++ .../preferences/keybindings.js | 316 ++++ .../preferences/service.js | 648 +++++++ .../gsconnect@andyholmes.github.io/prefs.js | 32 + .../schemas/gschemas.compiled | Bin 0 -> 5316 bytes ...ome.Shell.Extensions.GSConnect.gschema.xml | 192 ++ .../service/backends/lan.js | 881 +++++++++ .../service/components/__init__.js | 71 + .../service/components/atspi.js | 316 ++++ .../service/components/clipboard.js | 228 +++ .../service/components/contacts.js | 619 +++++++ .../service/components/input.js | 580 ++++++ .../service/components/mpris.js | 1005 +++++++++++ .../service/components/notification.js | 411 +++++ .../service/components/pulseaudio.js | 269 +++ .../service/components/session.js | 86 + .../service/components/sound.js | 180 ++ .../service/components/upower.js | 217 +++ .../service/components/ydotool.js | 167 ++ .../service/core.js | 698 ++++++++ .../service/daemon.js | 721 ++++++++ .../service/device.js | 1110 ++++++++++++ .../service/manager.js | 508 ++++++ .../service/nativeMessagingHost.js | 219 +++ .../service/plugin.js | 250 +++ .../service/plugins/battery.js | 433 +++++ .../service/plugins/clipboard.js | 182 ++ .../service/plugins/connectivity_report.js | 164 ++ .../service/plugins/contacts.js | 460 +++++ .../service/plugins/findmyphone.js | 249 +++ .../service/plugins/mousepad.js | 381 ++++ .../service/plugins/mpris.js | 906 ++++++++++ .../service/plugins/notification.js | 694 ++++++++ .../service/plugins/ping.js | 73 + .../service/plugins/presenter.js | 63 + .../service/plugins/runcommand.js | 254 +++ .../service/plugins/sftp.js | 488 +++++ .../service/plugins/share.js | 492 ++++++ .../service/plugins/sms.js | 536 ++++++ .../service/plugins/systemvolume.js | 204 +++ .../service/plugins/telephony.js | 245 +++ .../service/ui/contacts.js | 642 +++++++ .../service/ui/legacyMessaging.js | 228 +++ .../service/ui/messaging.js | 1320 ++++++++++++++ .../service/ui/mousepad.js | 462 +++++ .../service/ui/notification.js | 179 ++ .../service/ui/service.js | 252 +++ .../service/utils/dbus.js | 263 +++ .../service/utils/setup.js | 433 +++++ .../service/utils/ui.js | 53 + .../service/utils/uri.js | 171 ++ .../shell/clipboard.js | 380 ++++ .../shell/device.js | 380 ++++ .../shell/gmenu.js | 647 +++++++ .../shell/input.js | 38 + .../shell/keybindings.js | 103 ++ .../shell/notification.js | 454 +++++ .../shell/tooltip.js | 309 ++++ .../shell/utils.js | 283 +++ .../stylesheet.css | 127 ++ .../utils/remote.js | 519 ++++++ .../utils/remote.mjs | 12 + .../utils/setup.js | 51 + .../utils/setup.mjs | 12 + .../wl_clipboard.js | 298 ++++ 330 files changed, 47886 insertions(+) create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/BWClipboard.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/LICENSE create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/README.md create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/blur.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/carousel.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/extension.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/bing-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/brick-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/checked-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/fav-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/game-die-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/high-frame-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/low-frame-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/mid-frame-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-empty-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-full-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unchecked-symbolic.svg create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unfav-symbolic.svg create mode 100755 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ar/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ca/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/cs/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/de/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/en_AU/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/es/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/es_ES/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/fi_FI/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/fr/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/fr_FR/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/hu/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/id/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/it/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ja/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ko/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/nb/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/nl/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/nn/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pl/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pt/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pt_BR/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ru/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ru_RU/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/sv/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/tr/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/uk/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_CN/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_TW/LC_MESSAGES/BingWallpaper.mo create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/metadata.json create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/prefs.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/org.gnome.shell.extensions.bingwallpaper.gschema.xml create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/stylesheet.css create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/thumbnail.js create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/Settings4.ui create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/carousel4.ui create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/prefs.css create mode 100644 gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/utils.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/LICENSE create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/extension.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/helpers/file.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/battery-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/cpu-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/fan-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/memory-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-download-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-upload-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/storage-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/system-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/temperature-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/voltage-symbolic.svg create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ca/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/cs/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/de/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/es/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/fi_FI/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/fr/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/it/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/nl/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/oc/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pl/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt_BR/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ru/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/sk/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/tr/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/uk/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/zh_CN/LC_MESSAGES/vitals.mo create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/menuItem.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/metadata.json create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/prefs.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/prefs.ui create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/org.gnome.shell.extensions.vitals.gschema.xml create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/sensors.js create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/stylesheet.css create mode 100644 gnome/.local/share/extensions/Vitals@CoreCoding.com/values.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/extension.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/iconCache.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/fr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/hu/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/it/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ru/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/sk/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/sr@latin/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/tr/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/metadata.json create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/prefs.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js create mode 100644 gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/util.js create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/autoActivities.js create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/extension.js create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/metadata.json create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.js create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.ui create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/org.gnome.shell.extensions.auto-activities.gschema.xml create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/appfolders.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/applications.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/dash_to_dock.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/lockscreen.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/overview.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/panel.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/screenshot.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/components/window_list.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/connections.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/effects_manager.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/keys.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/settings.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/client.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/iface.xml create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/services.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.glsl create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.glsl create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/effects/paint_signals.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/extension.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/add-window-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/applications-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/dash-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/general-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/heart-filled-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/other-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/overview-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/remove-window-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/reset-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-mode-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-window-symbolic.svg create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ar/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/az/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/bg/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ca/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/cs/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/de/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/el/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/es/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/fr/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/he/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/hu/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/it/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ka/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ko/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/nb_NO/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/nl/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/nn/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/pl/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/pt/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/pt_BR/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ru/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sl/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sv/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ta/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/tr/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/uk/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/vi/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/locale/zh_Hans/LC_MESSAGES/blur-my-shell.mo create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/metadata.json create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/applications.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/customize_row.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/dash.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/general.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/menu.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/other.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/overview.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/panel.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/window_row.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/prefs.js create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/schemas/org.gnome.shell.extensions.blur-my-shell.gschema.xml create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/stylesheet.css create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/applications.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/customize-row.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/dash.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/general.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/menu.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/other.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/overview.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/panel.ui create mode 100644 gnome/.local/share/extensions/blur-my-shell@aunetx/ui/window-row.ui create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.mjs create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/extension.js create mode 100755 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/be/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ca/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/cs/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/de/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/el/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/es/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/et/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fa/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fi/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fy/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/gl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/hu/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/id/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ko/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/nl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/nl_BE/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/pl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/pt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ru/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr@latin/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sv/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/tr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/uk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/zh_TW/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/metadata.json create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/nautilus-gsconnect.py create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/org.gnome.Shell.Extensions.GSConnect.gresource create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/device.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/service.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/prefs.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/org.gnome.Shell.Extensions.GSConnect.gschema.xml create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/input.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/notification.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/session.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/sound.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/upower.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/ydotool.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/core.js create mode 100755 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/daemon.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/device.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/manager.js create mode 100755 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugin.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/service.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/setup.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/ui.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/device.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/input.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/notification.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/utils.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/stylesheet.css create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.mjs create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.js create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.mjs create mode 100644 gnome/.local/share/extensions/gsconnect@andyholmes.github.io/wl_clipboard.js diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/BWClipboard.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/BWClipboard.js new file mode 100644 index 0000000..279c65e --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/BWClipboard.js @@ -0,0 +1,34 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. + +import St from 'gi://St'; +import Gio from 'gi://Gio'; + +const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD; + +export default class BWClipboard { + constructor() { + this.clipboard = St.Clipboard.get_default(); + } + + setImage(filename) { + try { + let file = Gio.File.new_for_path(filename); + let [success, image_data] = file.load_contents(null); + //log('error: '+success); + if (success) + this.clipboard.set_content(CLIPBOARD_TYPE, 'image/jpeg', image_data); + } catch (err) { + log('unable to set clipboard to data in '+filename); + } + } + + setText(text) { + this.clipboard.set_text(CLIPBOARD_TYPE, text); + } +}; diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/LICENSE b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/LICENSE new file mode 100644 index 0000000..6b156fe --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/LICENSE @@ -0,0 +1,675 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/README.md b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/README.md new file mode 100644 index 0000000..9ac517a --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/README.md @@ -0,0 +1,99 @@ +# GNOME Shell extension - Bing Wallpaper + +Bring some color to your GNOME desktop by syncing your desktop and lockscreen wallpapers with today's Microsoft Bing image of the day (the image you see when you visit Bing.com) with this extension. The intention of this extension is to just do what it needs to do and stay out of your way, with a few optional features to improve quality-of-life. + +[![Get it on GNOME extensions](/screenshot/get_it_on_gnome_extensions.png)](https://extensions.gnome.org/extension/1262/bing-wallpaper-changer/) [![<3 Sponsor this project on GitHub <3](/screenshot/sponsor.png)](https://github.com/sponsors/neffo) + +![Screenshot](/screenshot/notification.png) + +As featured on [OMG! Ubuntu](https://www.omgubuntu.co.uk/2017/07/bing-wallpaper-changer-gnome-extension). + +Also, check out my related [Google Earth View wallpaper extension](https://github.com/neffo/earth-view-wallpaper-gnome-extension) and the partially-derived [Bing Desktop Wallpaper for Cinnamon](https://cinnamon-spices.linuxmint.com/applets/view/320) applet by Starcross. + +## Features + +* Automatically sets the Bing [Image of the Day](https://www.microsoft.com/en-us/bing/bing-wallpaper) as both lock screen and desktop wallpapers +* Only attempts to download wallpapers when they have been updated - doesn't poll continuously +* Shuffle/randomise wallpapers at adjustable intervals (including from your stored Bing images) +* Image gallery to view, select and curate stored images +* Optionally delete old images after a week, or you can keep (and curate) them forever +* Override the lockscreen blur (NEW: lockscreen blur is now dynamic!) +* Language support: English (en), German (de), Dutch (nl), Italian (it), Polish (pl), Chinese (zh_CN, zh_TW), French (fr_FR), Portuguese (pt, pt_BR), Ukrainian (uk), Russian (ru_RU), Spanish (es), Korean (ko), Indonesian (id), Catalan (ca), Norwegian Bokmål (nb) & Nynorsk (nn), Swedish (sv), Arabic (ar), Hungarian (hu), Japanese (ja), Czech (cs_CZ), Finnish (fi_FI) and Turkish (tr) - a HUGE thanks to the translators +* Image preview in menus & ability to manually set wallpapers individually or copy image to clipboard +* A selection of different theme-aware indicator (tray) icons to choose (or hide it completely) + +## TODO + +* add more languages (#14) - [please help if you can](https://github.com/neffo/bing-wallpaper-gnome-extension/issues/14) +* add user features requests - [lots have already been implemented](https://github.com/neffo/bing-wallpaper-gnome-extension/issues?q=is%3Aissue+label%3Aenhancement+is%3Aclosed) + +## Known Issues + +* In China, users are limited to 'Chinese – China', 'English - International' markets (this is the way Bing handles the Chinese market/locale, not an extension 'bug' - sorry!) +* Bing may detect your location incorrectly (and force a locale as above) - if you see this, please let me know what Bing.com itself does +* GNOME Shell themes can break some GNOME popup menu elements (toggle switches for example). This impacts GNOME more generally, not just this extension. Double check you are running latest versions of your themes (or disable them). + +## Requirements + +GNOME 3.36+ or 40+ (Ubuntu 20.04 LTS or later, older versions of the extension work with 3.18+, but are no longer supported). + +## Install + +[Install from extensions.gnome.org](https://extensions.gnome.org/extension/1262/bing-wallpaper-changer/) + +or install directly to your GNOME extensions directory (useful if you want to hack on it) + +``` +mkdir ~/Desktop/source +cd ~/Desktop/source +git clone https://github.com/neffo/bing-wallpaper-gnome-extension.git +cd bing-wallpaper-gnome-extension +sh install.sh +``` + +## Enable debug logging + +Enable debug logging through the exptension preferences 'Debug options' tab or if unable to open preferences you can enable debugging using dconf-editor with this command: +``` +GSETTINGS_SCHEMA_DIR=$HOME/.local/share/gnome-shell/extensions/BingWallpaper@ineffable-gmail.com/schemas dconf-editor /org/gnome/shell/extensions/bingwallpaper/ +``` + +Please include logs from your journal when submitting bug notices (make sure nothing sensitive is included in the text!). + +## Screenshots + +### Image gallery + +![Settings](/screenshot/settings5.png) + +### Preferences + +![Settings](/screenshot/settings.png) +![Settings](/screenshot/settings2.png) +![Settings](/screenshot/settings3.png) +![Settings](/screenshot/settings4.png) + + +### Lockscreen blur control +From left to right: +* no blur/no dimming +* slight blur/default dimming +* default blur/default dimming +![Blur example](/screenshot/blurexample.jpg) + +## Toss a coin to your coder + +Do you like this extension and want to show that you appreciate the work that goes into adding new features and keeping it maintained? Please consider buying me a coffee on [GitHub Sponsors](https://github.com/sponsors/neffo) or on [Flattr](https://flattr.com/@neffo). + +## Disclaimer + +This extension is unofficial and not affiliated with Bing or Microsoft in any way. Images are protected by copyright, and are licensed only for use as wallpapers. + +## Special Thanks + +This extension is based on the NASA APOD extension by [Elinvention](https://github.com/Elinvention) +and inspired by Bing Desktop WallpaperChanger by [Utkarsh Gupta](https://github.com/UtkarshGpta). Lockscreen blur code is based on [Pratap-Kumar's extension](https://github.com/PRATAP-KUMAR/Control_Blur_Effect_On_Lock_Screen). I'd like to give a special shout out to those who have [contributed code and translations](https://github.com/neffo/bing-wallpaper-gnome-extension/graphs/contributors) as well as everyone who has reported bugs or provided feedback and suggestions for improvements. Also, thanks to Microsoft for this great API and wallpaper collection. + +## License + +This extension is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/blur.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/blur.js new file mode 100644 index 0000000..e063be6 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/blur.js @@ -0,0 +1,159 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. +// Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod +// This code based on https://github.com/PRATAP-KUMAR/Control_Blur_Effect_On_Lock_Screen +// and https://github.com/sunwxg/gnome-shell-extension-unlockDialogBackground + +import St from 'gi://St'; +import * as UnlockDialog from 'resource:///org/gnome/shell/ui/unlockDialog.js'; +import * as Config from 'resource:///org/gnome/shell/misc/config.js'; +var _updateBackgroundEffects = UnlockDialog.UnlockDialog.prototype._updateBackgroundEffects; +var _showClock = UnlockDialog.UnlockDialog.prototype._showClock; +var _showPrompt = UnlockDialog.UnlockDialog.prototype._showPrompt; + +var shellVersionMajor = parseInt(Config.PACKAGE_VERSION.split('.')[0]); +var shellVersionMinor = parseInt(Config.PACKAGE_VERSION.split('.')[1]); +var shellVersionPoint = parseInt(Config.PACKAGE_VERSION.split('.')[2]); + +// default BWP mild blur +var BWP_BLUR_SIGMA = 2; +var BWP_BLUR_BRIGHTNESS = 55; +// GNOME defaults +var BLUR_BRIGHTNESS = 0.55; +var BLUR_SIGMA = 60; +var debug = false; + +var promptActive = false; // default GNOME method of testing this relies on state of a transisiton + // so we are being explicit here (do not want any races, thanks) + +function log(msg) { + if (debug) // set 'debug' above to false to keep the noise down in journal + console.log("BingWallpaper extension/Blur: " + msg); +} + +// we patch UnlockDialog._updateBackgroundEffects() +function _updateBackgroundEffects_BWP(monitorIndex) { + // GNOME shell 3.36.4 and above + log("_updateBackgroundEffects_BWP() called for shell >= 3.36.4"); + const themeContext = St.ThemeContext.get_for_stage(global.stage); + for (const widget of this._backgroundGroup.get_children()) { + // set blur effects, we have two modes in lockscreen: login prompt or clock + // blur on when clock is visible is adjustable + const effect = widget.get_effect('blur'); + if (promptActive) { + log('default blur active'); + if (effect) { + effect.set({ // GNOME defaults when login prompt is visible + brightness: BLUR_BRIGHTNESS, + sigma: BLUR_SIGMA * themeContext.scale_factor, + }); + } + } + else { + log('adjustable blur active'); + if (effect) { + effect.set({ // adjustable blur when clock is visible + brightness: BWP_BLUR_BRIGHTNESS * 0.01, // we use 0-100 rather than 0-1, so divide by 100 + sigma: BWP_BLUR_SIGMA * themeContext.scale_factor, + }); + } + } + } +} + +// we patch both UnlockDialog._showClock() and UnlockDialog._showPrompt() to let us +// adjustable blur in a Windows-like way (this ensures login prompt is readable) +function _showClock_BWP() { + promptActive = false; + this._showClock_GNOME(); // pass to default GNOME function + this._updateBackgroundEffects(); +} + +function _showPrompt_BWP() { + promptActive = true; + this._showPrompt_GNOME(); // pass to default GNOME function + this._updateBackgroundEffects(); +} + +export default class Blur { + constructor() { + this.enabled = false; + log('Bing Wallpaper adjustable blur is '+supportedVersion()?'available':'not available'); + } + + set_blur_strength(value) { + BWP_BLUR_SIGMA = this._clampValue(value); + log("lockscreen blur strength set to "+BWP_BLUR_SIGMA); + } + + set_blur_brightness(value) { + BWP_BLUR_BRIGHTNESS = this._clampValue(value); + log("lockscreen brightness set to " + BWP_BLUR_BRIGHTNESS); + } + + // valid values are 0 to 100 + _clampValue(value) { + if (value > 100) + value = 100; + if (value < 0 ) + value = 0; + return value; + } + + _switch(enabled) { + if (enabled && !this.enabled) { + this._enable(); + } + else { + this._disable(); + } + } + + _enable() { + if (supportedVersion()) { + log("Blur._enable() called on GNOME "+Config.PACKAGE_VERSION); + UnlockDialog.UnlockDialog.prototype._updateBackgroundEffects = _updateBackgroundEffects_BWP; + // we override _showClock and _showPrompt to patch in updates to blur effect before calling the GNOME functions + UnlockDialog.UnlockDialog.prototype._showClock = _showClock_BWP; + UnlockDialog.UnlockDialog.prototype._showPrompt = _showPrompt_BWP; + + // this are the original functions which we call into from our versions above + UnlockDialog.UnlockDialog.prototype._showClock_GNOME = _showClock; + UnlockDialog.UnlockDialog.prototype._showPrompt_GNOME = _showPrompt; + + } + this.enabled = true; + } + + _disable() { + if (!this.enabled) + return; + log("_lockscreen_blur_disable() called"); + if (supportedVersion()) { + // restore default functions + UnlockDialog.UnlockDialog.prototype._updateBackgroundEffects = _updateBackgroundEffects; + UnlockDialog.UnlockDialog.prototype._showClock = _showClock; + UnlockDialog.UnlockDialog.prototype._showPrompt = _showPrompt; + // clean up unused functions we created + UnlockDialog.UnlockDialog.prototype._showClock_GNOME = null; + delete UnlockDialog.UnlockDialog.prototype._showClock_GNOME; + UnlockDialog.UnlockDialog.prototype._showPrompt_GNOME = null; + delete UnlockDialog.UnlockDialog.prototype._showPrompt_GNOME; + } + this.enabled = false; + } +}; + +function supportedVersion() { // when current lockscren blur implementation was first shipped (we ignore earlier weird version) + if (shellVersionMajor >= 40 || + (shellVersionMajor == 3 && shellVersionMinor == 36 && shellVersionPoint >= 4)) { + return true; + } + + return false; +} diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/carousel.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/carousel.js new file mode 100644 index 0000000..c673b14 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/carousel.js @@ -0,0 +1,228 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. +// Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod + +import Gtk from 'gi://Gtk'; +import GdkPixbuf from 'gi://GdkPixbuf'; +import Gio from 'gi://Gio'; +import * as Utils from './utils.js'; +import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +const default_dimensions = [30, 30, 1650, 800]; // TODO: pull from and save dimensions to settings, but perhaps verify that dimensions are ok + +const GALLERY_THUMB_WIDTH = 320; +const GALLERY_THUMB_HEIGHT = 180; + +export default class Carousel { + constructor(settings, button = null, callbackfunc = null, prefs_flowbox = null, extensionPath = null) { + //create_gallery(widget, settings); + this.settings = settings; + this.button = button; + this.callbackfunc = callbackfunc; + this.flowBox = null; + this.window = null; + this.imageList = Utils.imageListSortByDate(Utils.getImageList(this.settings)).reverse(); // get images and reverse order + this.searchEntry = null; + this.extensionPath = extensionPath + + this.log('create carousel...'); + + this.flowBox = prefs_flowbox; + this.flowBox.insert(this._create_placeholder_item(), -1); + } + + _enable_button() { + if (this.button) { + this.button.set_sensitive(state); + } + } + + _create_gallery() { + Utils.randomIntervals.forEach((x) => { + let item = this._create_random_item(x.value, _(x.title)); + this.flowBox.insert(item, -1); + }); + + this.imageList.forEach((image) => { + let item = this._create_gallery_item(image); + this.flowBox.insert(item, -1); + }); + } + + _create_gallery_item(image) { + let buildable = new Gtk.Builder(); + + // grab appropriate object from UI file + buildable.add_objects_from_file(this.extensionPath + '/ui/carousel4.ui', ["flowBoxChild"]); + + // assign variables to the UI objects we've just loaded + let galleryImage = buildable.get_object('galleryImage'); + let filename = Utils.imageToFilename(this.settings, image); + let viewButton = buildable.get_object('viewButton'); + let applyButton = buildable.get_object('applyButton'); + let infoButton = buildable.get_object('infoButton'); + let deleteButton = buildable.get_object('deleteButton'); + let favButton = buildable.get_object('favButton'); + let unfavButton = buildable.get_object('unfavButton'); + + if (Utils.isFavourite(image)) { + favButton.set_visible(false); + this.log('image is favourited'); + } + else { + unfavButton.set_visible(false); + } + + try { + this._load_image(galleryImage, filename); + } + catch (e) { + galleryImage.set_from_icon_name('image-missing'); + galleryImage.set_icon_size = 2; // Gtk.GTK_ICON_SIZE_LARGE; + this.log('create_gallery_image: '+e); + } + + galleryImage.set_tooltip_text(image.copyright); + + // set up actions for when a image button is clicked + viewButton.connect('clicked', () => { + Utils.openInSystemViewer(filename); + }); + + applyButton.connect('clicked', () => { + this.settings.set_string('selected-image', Utils.getImageUrlBase(image)); + this.log('gallery selected '+Utils.getImageUrlBase(image)); + }); + + infoButton.connect('clicked', () => { + Utils.openInSystemViewer(image.copyrightlink, false); + this.log('info page link opened '+image.copyrightlink); + }); + + deleteButton.connect('clicked', (widget) => { + this.log('Delete requested for '+filename); + Utils.deleteImage(filename); + //Utils.cleanupImageList(this.settings); // hide image instead + Utils.hideImage(this.settings, [image]); + widget.get_parent().get_parent().set_visible(false); // bit of a hack + if (this.callbackfunc) + this.callbackfunc(); + }); + + // button is unchecked, so we want to make the checked one visible + favButton.connect('clicked', (widget) => { + this.log('favourited '+Utils.getImageUrlBase(image)); + widget.set_visible(false); + unfavButton.set_visible(true); + Utils.setImageFavouriteStatus(this.settings, image.urlbase, true); + }); + + // button is checked, so we want to make the unchecked one visible + unfavButton.connect('clicked', (widget) => { + this.log('unfavourited '+Utils.getImageUrlBase(image)); + widget.set_visible(false); + favButton.set_visible(true); + Utils.setImageFavouriteStatus(this.settings, image.urlbase, false); + }); + + let item = buildable.get_object('flowBoxChild'); + return item; + } + + _create_random_item(interval, title) { + let buildable = new Gtk.Builder(); + + // grab appropriate object from UI file + buildable.add_objects_from_file(this.extensionPath + '/ui/carousel4.ui', ["flowBoxRandom"]); + + let randomLabel = buildable.get_object('randomLabel'); + randomLabel.set_text(title); + let filename = 'random'; + let applyButton = buildable.get_object('randomButton'); + + applyButton.connect('clicked', (widget) => { + this.settings.set_string('random-interval-mode', interval); + this.settings.set_boolean('random-mode-enabled', true); + this.log('gallery selected random with interval '+interval+' ('+title+')'); + }); + + let item = buildable.get_object('flowBoxRandom'); + return item; + } + + _create_placeholder_item() { + let buildable = new Gtk.Builder(); + this.flowBox.set_max_children_per_line(1); + + // grab appropriate object from UI file + buildable.add_objects_from_file(this.extensionPath + '/ui/carousel4.ui', ["flowBoxPlaceholder"]); + + let loadButton = buildable.get_object('loadButton'); + + loadButton.connect('clicked', (widget) => { + widget.set_label(_("Loading...")); // does this work??? + this.flowBox.remove(widget.get_parent()); + this.flowBox.set_max_children_per_line(2); + this._create_gallery(); + }); + + let item = buildable.get_object('flowBoxPlaceholder'); + return item; + } + + _load_image(galleryImage, filename) { + let thumb_path = Utils.getWallpaperDir(this.settings)+'.thumbs/'; + let thumb_dir = Gio.file_new_for_path(thumb_path); + let save_thumbs = !this.settings.get_boolean('delete-previous') && this.settings.get_boolean('create-thumbs'); // create thumbs only if not deleting previous and thumbs are enabled + + if (!thumb_dir.query_exists(null)) { + thumb_dir.make_directory_with_parents(null); + } + + let image_file = Gio.file_new_for_path(filename); + + // load gallery image or create new thumbnail if it doesn't + if (!image_file.query_exists(null)){ + this._set_blank_image(galleryImage); + } + else { + let image_thumb_path = thumb_path + image_file.get_basename(); + let image_thumb = Gio.file_new_for_path(image_thumb_path); + + try { + let pixbuf; + + // use thumbnail if available + if (image_thumb.query_exists(null)) { + pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_thumb_path); + } + else { // save changed thumbnail significantly speeds up gallery loading, but costs some addtional disk space + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, GALLERY_THUMB_WIDTH, GALLERY_THUMB_HEIGHT); + if (save_thumbs) + pixbuf.savev(image_thumb_path,'jpeg',['quality'], ['90']); + } + + galleryImage.set_pixbuf(pixbuf); + } + catch (e) { + this._set_blank_image(galleryImage); + this.log('create_gallery_image: '+e); + } + } + } + + _set_blank_image(galleryImage) { + //galleryImage.set_from_icon_name('image-missing'); + //galleryImage.set_icon_size = 2; // Gtk.GTK_ICON_SIZE_LARGE; + } + + log(msg) { + if (this.settings.get_boolean('debug-logging')) + console.log("BingWallpaper extension: " + msg); // disable to keep the noise down in journal + } +}; \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/extension.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/extension.js new file mode 100644 index 0000000..e286d0f --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/extension.js @@ -0,0 +1,1104 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. +// Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod + +import St from 'gi://St'; +import Soup from 'gi://Soup'; +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; +import GLib from 'gi://GLib'; +import Clutter from 'gi://Clutter'; +import Cogl from 'gi://Cogl'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; +import {Button} from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import * as Config from 'resource:///org/gnome/shell/misc/config.js'; + +import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; +import * as Utils from './utils.js'; +import Blur from './blur.js'; +import Thumbnail from './thumbnail.js'; +import BWClipboard from './BWClipboard.js'; + +const BingImageURL = Utils.BingImageURL; +const BingURL = 'https://www.bing.com'; +const IndicatorName = 'BingWallpaperIndicator'; +const TIMEOUT_SECONDS = 24 * 3600; // FIXME: this should use the end data from the json data +const TIMEOUT_SECONDS_ON_HTTP_ERROR = 1 * 3600; // retry in one hour if there is a http error3 +const MINIMUM_SHUFFLE_IMAGES = 3; // bare minimum to use filtered image set in shuffle mode +const ICON_PREVIOUS_BUTTON = 'media-seek-backward-symbolic'; +const ICON_NEXT_BUTTON = 'media-seek-forward-symbolic'; +const ICON_CURRENT_BUTTON = 'media-skip-forward-symbolic'; + +let bingWallpaperIndicator = null; +let blur = null; + +const newMenuItem = (label) => { + return new PopupMenu.PopupMenuItem(label); +} + +const newMenuSwitchItem = (label, state) => { + let switchItem = new PopupMenu.PopupSwitchMenuItem( + label, + state, + {}); + switchItem.label.x_expand = true; + switchItem._statusBin.x_expand = false; + return switchItem; +} + +function log(msg) { + if (bingWallpaperIndicator && bingWallpaperIndicator._settings.get_boolean('debug-logging')) + console.log('BingWallpaper extension: ' + msg); // disable to keep the noise down in journal +} + +function notifyError(msg) { + Main.notifyError("BingWallpaper extension error", msg); +} + +function doSetBackground(uri, schema) { + let gsettings = new Gio.Settings({schema: schema}); + let prev = gsettings.get_string('picture-uri'); + uri = 'file://' + uri; + gsettings.set_string('picture-uri', uri); + try { + gsettings.set_string('picture-uri-dark', uri); + } + catch (e) { + log("unable to set dark background for : " + e); + } + Gio.Settings.sync(); + gsettings.apply(); + return (prev != uri); // return true if background uri has changed +} + +const BingWallpaperIndicator = GObject.registerClass( +class BingWallpaperIndicator extends Button { + _init(ext) { + super._init(0, IndicatorName, false); + + this.title = ""; + this.explanation = ""; + this.filename = ""; + this.copyright = ""; + this.version = "0.1"; + this._updatePending = false; + this._timeout = null; + this._shuffleTimeout = null; + this.longstartdate = null; + this.imageURL = ""; // link to image itself + this.imageinfolink = ""; // link to Bing photo info page + this.refreshdue = 0; + this.shuffledue = 0; + this.refreshduetext = ""; + this.thumbnail = null; + this.thumbnailItem = null; + this.selected_image = "current"; + this.clipboard = new BWClipboard(); + this.imageIndex = null; + this.logger = null; + this.favourite_status = false; + this.hidden_status = false; + this.dimensions = { 'width': null, 'height': null}; + this._extension = ext; + + let extensionIconsPath = ext.dir.get_child('icons').get_path(); + this.ICON_RANDOM = extensionIconsPath + '/'+'game-die-symbolic.svg'; + this.ICON_FAVE_BUTTON = extensionIconsPath + '/'+'fav-symbolic.svg'; + this.ICON_UNFAVE_BUTTON = extensionIconsPath + '/'+'unfav-symbolic.svg'; + this.ICON_TRASH_BUTTON = extensionIconsPath + '/'+'trash-empty-symbolic.svg'; + this.ICON_UNTRASH_BUTTON = extensionIconsPath + '/'+'trash-full-symbolic.svg'; + + if (!blur) // as Blur isn't disabled on screen lock (like the rest of the extension is) + blur = new Blur(); + + // take a variety of actions when the gsettings values are modified by prefs + this._settings = this._extension.getSettings(); + + // create Soup session + this._initSoup(); + + this.visible = !this._settings.get_boolean('hide'); + + this.refreshDueItem = newMenuItem(_("")); + this.explainItem = newMenuItem(_("Awaiting refresh...")); + this.copyrightItem = newMenuItem(_("Awaiting refresh...")); + this.clipboardImageItem = newMenuItem(_("Copy image to clipboard")); + this.clipboardURLItem = newMenuItem(_("Copy image URL to clipboard")); + this.folderItem = newMenuItem(_("Open image folder")); + this.dwallpaperItem = newMenuItem(_("Set background image")); + this.swallpaperItem = newMenuItem(_("Set lock screen image")); + this.refreshItem = newMenuItem(_("Refresh Now")); + this.settingsItem = newMenuItem(_("Settings")); + this.openImageItem = newMenuItem(_("Open in image viewer")); + this.openImageInfoLinkItem = newMenuItem(_("Open Bing image information page")); + this.imageResolutionItem = newMenuItem(_("Awaiting refresh...")); + + this.titleItem = new PopupMenu.PopupSubMenuMenuItem(_("Awaiting refresh..."), false); + [this.imageResolutionItem, this.openImageInfoLinkItem, this.openImageItem, this.folderItem, + this.clipboardImageItem, this.clipboardURLItem, this.dwallpaperItem] + .forEach(e => this.titleItem.menu.addMenuItem(e)); + + // quick settings submenu + this.settingsSubMenu = new PopupMenu.PopupSubMenuMenuItem(_("Quick settings"), false); + // toggles under the quick settings submenu + this.toggleSetBackground = newMenuSwitchItem(_("Set background image"), this._settings.get_boolean('set-background')); + this.toggleSelectNew = newMenuSwitchItem(_("Always show new images"), this._settings.get_boolean('revert-to-current-image')); + this.toggleShuffle = newMenuSwitchItem(_("Image shuffle mode"), true); + this.toggleShuffleOnlyFaves = newMenuSwitchItem(_("Image shuffle only favourites"), this._settings.get_boolean('random-mode-include-only-favourites')); + this.toggleNotifications = newMenuSwitchItem(_("Enable desktop notifications"), this._settings.get_boolean('notify')); + this.toggleImageCount = newMenuSwitchItem(_("Show image count"), this._settings.get_boolean('show-count-in-image-title')); + this.toggleShuffleOnlyUHD = newMenuSwitchItem(_("Image shuffle only UHD resolutions"), this._settings.get_boolean('random-mode-include-only-uhd')); + + [this.toggleNotifications, /*this.toggleImageCount, this.toggleSetBackground,*/ this.toggleSelectNew, + this.toggleShuffle, this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD] + .forEach(e => this.settingsSubMenu.menu.addMenuItem(e)); + + // these items are a bit unique, we'll populate them in _setControls() + this.controlItem = newMenuItem(""); + this.thumbnailItem = new PopupMenu.PopupBaseMenuItem({ style_class: 'wp-thumbnail-image'}); + this._setControls(); // build the button bar + + // we need to word-wrap these menu items to not overflow menu in case of long lines of text + [this.refreshDueItem, this.titleItem, this.explainItem, this.copyrightItem] + .forEach((e, i) => { + this._wrapLabelItem(e); + }); + + // set the order of menu items (including separators) + let allMenuItems = [ + this.refreshItem, + this.refreshDueItem, + new PopupMenu.PopupSeparatorMenuItem(), + this.controlItem, + new PopupMenu.PopupSeparatorMenuItem(), + this.explainItem, + this.thumbnailItem, + this.titleItem, + this.copyrightItem, + new PopupMenu.PopupSeparatorMenuItem(), + this.settingsSubMenu, + this.settingsItem + ]; + allMenuItems.forEach(e => this.menu.addMenuItem(e)); + + // non clickable information items + [this.explainItem, this.copyrightItem, this.refreshDueItem, this.thumbnailItem, this.imageResolutionItem] + .forEach((e) => { + e.setSensitive(false); + }); + + if (this._settings.get_boolean('random-mode-enabled')) { + [this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD] + .forEach((e) => { + e.setSensitive(false); + }); + } + + this._setConnections(); + + if (this._settings.get_string('state') != '[]') { // setting state on reset or initial boot + this._reStoreState(); + } + else { + this._restartTimeout(60); // wait 60 seconds before performing refresh + } + } + + // create Soup session + _initSoup() { + this.httpSession = new Soup.Session(); + this.httpSession.user_agent = 'User-Agent: Mozilla/5.0 (X11; GNOME Shell/' + Config.PACKAGE_VERSION + '; Linux x86_64; +https://github.com/neffo/bing-wallpaper-gnome-extension ) BingWallpaper Gnome Extension/' + this._extension.metadata.version; + } + + // listen for configuration changes + _setConnections() { + this._settings.connect('changed::hide', () => { + this.visible = !this._settings.get_boolean('hide'); + }); + + let settingConnections = [ + {signal: 'changed::icon-name', call: this._setIcon}, + {signal: 'changed::market', call: this._refresh}, + {signal: 'changed::set-background', call: this._setBackground}, + {signal: 'changed::override-lockscreen-blur', call: this._setBlur}, + {signal: 'changed::selected-image', call: this._setImage}, + {signal: 'changed::delete-previous', call: this._cleanUpImages}, + {signal: 'changed::notify', call: this._notifyCurrentImage}, + {signal: 'changed::always-export-bing-json', call: this._exportData}, + {signal: 'changed::bing-json', call: this._exportData}, + {signal: 'changed::controls-icon-size', call: this._setControls}, + {signal: 'changed::random-mode-enabled', call: this._randomModeChanged}, + {signal: 'changed::random-mode-include-only-favourites', call: this._randomModeChanged}, + {signal: 'changed::random-mode-include-only-unhidden', call: this._randomModeChanged}, + {signal: 'changed::random-mode-include-only-uhd', call: this._randomModeChanged}, + {signal: 'changed::random-interval-mode', call: this._randomModeChanged} + ]; + + // _setShuffleToggleState + settingConnections.forEach((e) => { + this._settings.connect(e.signal, e.call.bind(this)); + }); + + this._settings.connect('changed::lockscreen-blur-strength', blur.set_blur_strength.bind(this, this._settings.get_int('lockscreen-blur-strength'))); + this._settings.connect('changed::lockscreen-blur-brightness', blur.set_blur_brightness.bind(this, this._settings.get_int('lockscreen-blur-brightness'))); + + // ensure we're in a sensible initial state + this._setIcon(); + this._setBlur(); + this._setImage(); + this._cleanUpImages(); + + // menu connections + this.connect('button-press-event', this._openMenu.bind(this)); + + // link menu items to functions + //this.thumbnailItem.connect('activate', this._setBackgroundDesktop.bind(this)); + this.thumbnailItem.connect('activate', this._openInSystemViewer.bind(this)); + this.openImageItem.connect('activate', this._openInSystemViewer.bind(this)); + //this.titleItem.connect('activate', this._setBackgroundDesktop.bind(this)); + this.openImageInfoLinkItem.connect('activate', this._openImageInfoLink.bind(this)); + this.dwallpaperItem.connect('activate', this._setBackgroundDesktop.bind(this)); + this.refreshItem.connect('activate', this._refresh.bind(this)); + this.settingsItem.connect('activate', this._openPrefs.bind(this)); + + // unfortunately we can't bind toggles trivially like we can with prefs.js here, so we handle toggles in two steps + // first, we listen for changes to these toggle settings and update the status + // & then, link settings to toggle state (the other way) + + let toggles = [ /*{key: 'set-background', toggle: this.toggleSetBackground},*/ + {key: 'revert-to-current-image', toggle: this.toggleSelectNew}, + {key: 'notify', toggle: this.toggleNotifications}, + /*{key: 'show-count-in-image-title', toggle: this.toggleImageCount},*/ + {key: 'random-mode-enabled', toggle: this.toggleShuffle}, + {key: 'random-mode-include-only-favourites', toggle: this.toggleShuffleOnlyFaves}, + /*{key: 'random-mode-include-only-unhidden', toggle: this.toggleShuffleOnlyUnhidden},*/ + {key: 'random-mode-include-only-uhd', toggle: this.toggleShuffleOnlyUHD}]; + + toggles.forEach( (e) => { + this._settings.connect('changed::'+e.key, () => { + e.toggle.setToggleState(this._settings.get_boolean(e.key)); + }); + e.toggle.connect('toggled', (item, state) => { + this._settings.set_boolean(e.key, state); + }); + }); + + this.folderItem.connect('activate', Utils.openImageFolder.bind(this, this._settings)); + if (this.clipboard.clipboard) { // only if we have a clipboard + this.clipboardImageItem.connect('activate', this._copyImageToClipboard.bind(this)); + this.clipboardURLItem.connect('activate', this._copyURLToClipboard.bind(this)); + } + else { + [this.clipboardImageItem, this.clipboardURLItem]. + forEach(e => e.setSensitive(false)); + } + } + + _openPrefs() { + this._extension.openPreferences(); + } + + _openMenu() { + // Grey out menu items if an update is pending + this.refreshItem.setSensitive(!this._updatePending); + this.clipboardImageItem.setSensitive(!this._updatePending && this.imageURL != ""); + this.clipboardURLItem.setSensitive(!this._updatePending && this.imageURL != ""); + this.thumbnailItem.setSensitive(!this._updatePending && this.imageURL != ""); + //this.showItem.setSensitive(!this._updatePending && this.title != "" && this.explanation != ""); + this.dwallpaperItem.setSensitive(!this._updatePending && this.filename != ""); + this.swallpaperItem.setSensitive(!this._updatePending && this.filename != ""); + this.titleItem.setSensitive(!this._updatePending && this.imageinfolink != ""); + let maxlongdate = Utils.getMaxLongDate(this._settings); + this.refreshduetext = + _("Next refresh") + ": " + (this.refreshdue ? this.refreshdue.format("%Y-%m-%d %X") : '-') + + " (" + Utils.friendly_time_diff(this.refreshdue) + ")\n" + + _("Last refresh") + ": " + (maxlongdate? this._localeDate(maxlongdate, true) : '-'); + // also show when shuffle is next due + if (this._settings.get_boolean('random-mode-enabled')) { + this.refreshduetext += "\n" + _("Next shuffle")+": " + + (this.shuffledue ? this.shuffledue.format("%Y-%m-%d %X") : '-') + + " (" + Utils.friendly_time_diff(this.shuffledue) + ")"; + } + this.refreshDueItem.label.set_text(this.refreshduetext); + } + + _setBlur() { + blur._switch(this._settings.get_boolean('override-lockscreen-blur')); + blur.set_blur_strength(this._settings.get_int('lockscreen-blur-strength')); + blur.set_blur_brightness(this._settings.get_int('lockscreen-blur-brightness')); + } + + _setImage() { + Utils.validate_imagename(this._settings); + this.selected_image = this._settings.get_string('selected-image'); + log('selected image changed to: ' + this.selected_image); + this._selectImage(); + //this._setShuffleToggleState(); + } + + _notifyCurrentImage() { + if (this._settings.get_boolean('notify')) { + let image = this._getCurrentImage(); + if (image) { + this._createNotification(image); + } + } + } + + // set indicator icon (tray icon) + _setIcon() { + Utils.validate_icon(this._settings, this._extension.path); + let icon_name = this._settings.get_string('icon-name'); + let gicon = Gio.icon_new_for_string(this._extension.dir.get_child('icons').get_path() + '/' + icon_name + '.svg'); + this.icon = new St.Icon({gicon: gicon, style_class: 'system-status-icon'}); + log('Replace icon set to: ' + icon_name); + this.remove_all_children(); + this.add_child(this.icon); + } + + // set backgrounds as requested and set preview image in menu + _setBackground() { + if (this.filename == '') + return; + this.thumbnail = new Thumbnail(this.filename, St.ThemeContext.get_for_stage(global.stage).scale_factor); // use scale factor to make them look nicer + this._setThumbnailImage(); + if (!this.dimensions.width || !this.dimensions.height) // if dimensions aren't in image database yet + [this.dimensions.width, this.dimensions.height] = Utils.getFileDimensions(this.filename); + log('image set to : '+this.filename); + if (this._settings.get_boolean('set-background')) + this._setBackgroundDesktop(); + } + + _setBackgroundDesktop() { + doSetBackground(this.filename, Utils.DESKTOP_SCHEMA); + } + + _copyURLToClipboard() { + this.clipboard.setText(this.imageURL); + } + + _copyImageToClipboard() { + this.clipboard.setImage(this.filename); + } + + // set a timer on when the current image is going to expire + _restartTimeoutFromLongDate(longdate) { + // all Bing times are in UTC (+0) + let refreshDue = Utils.dateFromLongDate(longdate, 86400).to_local(); + let now = GLib.DateTime.new_now_local(); + let difference = refreshDue.difference(now) / 1000000; + + if (difference < 60 || difference > 86400) // clamp to a reasonable range + difference = 60; + difference = difference + 300; // 5 minute fudge offset in case of inaccurate local clock + + log('Next refresh due ' + difference + ' seconds from now'); + this._restartTimeout(difference); + } + + // alternative shuffle mode, not yet enabled + _restartShuffleTimeoutFromDueDate(duedate) { + let now = GLib.DateTime.new_now_local(); + let difference = duedate.difference(now) / 1000000; + if (difference < 60 || difference > 86400) // clamp to a reasonable range + difference = 60; + + log('Next shuffle due ' + difference + ' seconds from now'); + this._restartShuffleTimeout(difference); + } + + // convert longdate format into human friendly format + _localeDate(longdate, include_time = false) { + try { + let date = Utils.dateFromLongDate(longdate, 300); // date at update + return date.to_local().format('%Y-%m-%d' + (include_time? ' %X' : '')); // ISO 8601 - https://xkcd.com/1179/ + } + catch (e) { + return 'none'; + } + } + + // set menu text in lieu of a notification/popup + _setMenuText() { + this.titleItem.label.set_text(this.title ? this.title : ''); + this.copyrightItem.label.set_text(this.copyright ? this.copyright : ''); + this.imageResolutionItem.label.set_text(this.dimensions.width+'px x '+this.dimensions.height+'px'); + if (this._settings.get_boolean('show-count-in-image-title') && this.explanation) { + let imageList = JSON.parse(this._settings.get_string('bing-json')); + if (imageList.length > 0) + this.explainItem.label.set_text( this.explanation + ' [' + (this.imageIndex + 1) + '/' + imageList.length + ']'); + } + else { + this.explainItem.label.set_text(this.explanation ? this.explanation : ''); + } + this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON); + this._setTrashIcon(this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON); + } + + _wrapLabelItem(menuItem) { + let clutter_text = menuItem.label.get_clutter_text(); + clutter_text.set_line_wrap(true); + clutter_text.set_ellipsize(0); + clutter_text.set_max_length(0); + menuItem.label.set_style('max-width: 420px;'); + } + + _setControls() { + this.favouriteBtn = this._newMenuIcon( + this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON, + this.controlItem, + this._favouriteImage); + this.trashBtn = this._newMenuIcon( + this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON, + this.controlItem, + this._trashImage); + this.prevBtn = this._newMenuIcon( + ICON_PREVIOUS_BUTTON, + this.controlItem, + this._prevImage); + this.nextBtn = this._newMenuIcon( + ICON_NEXT_BUTTON, + this.controlItem, + this._nextImage); + this.curBtn = this._newMenuIcon( + ICON_CURRENT_BUTTON, + this.controlItem, + this._curImage); + this.randomizeBtn = this._newMenuIcon( + this.ICON_RANDOM, + this.controlItem, + this._selectImage, + null, true); + } + + _newMenuIcon(icon_name, parent, fn, position = null, arg = null) { + let gicon = Gio.icon_new_for_string(icon_name); + let icon = new St.Icon({ + /*icon_name: icon_name,*/ + gicon: gicon, + style_class: 'popup-menu-icon', + x_expand: true, + y_expand: true, + icon_size: this._settings.get_int('controls-icon-size') + }); + + let iconBtn = new St.Button({ + style_class: 'ci-action-btn', + can_focus: true, + child: icon, + /* x_align: Clutter.ActorAlign.END, // FIXME: errors on GNOME 3.28, default to center is ok */ + x_expand: true, + y_expand: true + }); + + if (position !== null) { + parent.insert_child_at_index(iconBtn, position); + } + else { + parent.add_child(iconBtn); + } + + iconBtn.connect('button-press-event', fn.bind(this, arg)); + return iconBtn; + } + + // set menu thumbnail + _setThumbnailImage() { + let pixbuf = this.thumbnail.pixbuf; + let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + + if (pixbuf == null) + return; + + const {width, height} = pixbuf; + + if (height == 0) { + return; + } + + const image = new Clutter.Image(); + const success = image.set_data( + pixbuf.get_pixels(), + pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888, + width, + height, + pixbuf.get_rowstride() + ); + + if (!success) { + throw Error("error creating Clutter.Image()"); + } + + this.thumbnailItem.hexpand = false; + this.thumbnailItem.vexpand = false; + this.thumbnailItem.content = image; + + log('scale factor: ' + scale_factor); + this.thumbnailItem.set_size(480*scale_factor, 270*scale_factor); + this.thumbnailItem.setSensitive(true); + } + + _nextImage() { + this._gotoImage(1); + } + + _prevImage() { + this._gotoImage(-1); + } + + _curImage() { + this._settings.set_string('selected-image', 'current'); + this._gotoImage(0); + } + + _randomModeChanged() { + let randomEnabled = this._settings.get_boolean('random-mode-enabled'); + [this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD /*, this.toggleShuffleOnlyUnhidden*/] + .forEach( x => { + x.setSensitive(randomEnabled); + }); + if (randomEnabled) { + log('enabled shuffle mode, by setting a shuffe timer (5 seconds)'); + this._restartShuffleTimeout(5); + this._settings.set_boolean('revert-to-current-image', false); + } + else { + // clear shuffle timer + if (this._shuffleTimeout) + GLib.source_remove(this._shuffleTimeout); + this._settings.set_boolean('revert-to-current-image', true); + } + } + + _favouriteImage() { + log('favourite image '+this.imageURL+' status was '+this.favourite_status); + this.favourite_status = !this.favourite_status; + Utils.setImageFavouriteStatus(this._settings, this.imageURL, this.favourite_status); + this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON); + } + + _trashImage() { + log('trash image '+this.imageURL+' status was '+this.hidden_status); + this.hidden_status = !this.hidden_status; + Utils.setImageHiddenStatus(this._settings, [this.imageURL], this.hidden_status); + this._setTrashIcon(this.hidden_status?ICON_UNTRASH_BUTTON:ICON_TRASH_BUTTON); + } + + _setFavouriteIcon(icon_name) { + let gicon = Gio.icon_new_for_string(icon_name); + this.favouriteBtn.get_children().forEach( (x, i) => { + x.set_gicon(gicon); + }); + } + + _setTrashIcon(icon_name) { + let gicon = Gio.icon_new_for_string(icon_name); + this.trashBtn.get_children().forEach( (x, i) => { + x.set_gicon(gicon); + }); + } + + _gotoImage(relativePos) { + let imageList = Utils.getImageList(this._settings); + let curIndex = 0; + + if (this.selected_image == 'current') { + curIndex = Utils.getCurrentImageIndex(imageList); + } + else { + curIndex = Utils.imageIndex(imageList, this.selected_image); + } + + let newImage = Utils.getImageByIndex(imageList, curIndex + relativePos); + + if (newImage) + this._settings.set_string('selected-image', newImage.urlbase.replace('/th?id=OHR.', '')); + } + + _getCurrentImage() { + let imageList = Utils.getImageList(this._settings); + let curIndex = Utils.getCurrentImageIndex(imageList); + + return Utils.getImageByIndex(imageList, curIndex); + } + + // download Bing metadata + _refresh() { + if (this._updatePending) + return; + this._updatePending = true; + this._restartTimeout(); + + let market = this._settings.get_string('market'); + if (Soup.MAJOR_VERSION >= 3) { + let url = BingImageURL; + let params = Utils.BingParams; + params['mkt'] = ( market != 'auto' ? market : '' ); + + let request = Soup.Message.new_from_encoded_form('GET', url, Soup.form_encode_hash(params)); + request.request_headers.append('Accept', 'application/json'); + + try { + this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => { + this._processMessageRefresh(message); + }); + } + catch(error) { + log('unable to send libsoup json message '+error); + } + } + else { + let url = BingImageURL + '?format=js&idx=0&n=8&mbl=1&mkt=' + (market != 'auto' ? market : ''); + let request = Soup.Message.new('GET', url); + request.request_headers.append('Accept', 'application/json'); + + // queue the http request + try { + this.httpSession.queue_message(request, (httpSession, message) => { + this._processMessageRefresh(message); + }); + } + catch (error) { + log('unable to send libsoup json message '+error); + } + } + } + + _processMessageRefresh(message) { + const decoder = new TextDecoder(); + try { + let data = (Soup.MAJOR_VERSION >= 3) ? + decoder.decode(this.httpSession.send_and_read_finish(message).get_data()): // Soup3 + message.response_body.data; // Soup 2 + + log('Recieved ' + data.length + ' bytes'); + this._parseData(data); + + if (!this._settings.get_boolean('random-mode-enabled')) + this._selectImage(); + } + catch (error) { + log('Network error occured: ' + error); + this._updatePending = false; + this._restartTimeout(TIMEOUT_SECONDS_ON_HTTP_ERROR); + } + } + + // sets a timer for next refresh of Bing metadata + _restartTimeout(seconds = null) { + if (this._timeout) + GLib.source_remove(this._timeout); + + if (seconds == null) + seconds = TIMEOUT_SECONDS; + + this._timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._refresh.bind(this)); + this.refreshdue = GLib.DateTime.new_now_local().add_seconds(seconds); + log('next check in ' + seconds + ' seconds'); + } + + _restartShuffleTimeout(seconds = null) { + log('_restartShuffleTimeout('+seconds+')'); + //console.trace(); + + if (this._shuffleTimeout) + GLib.source_remove(this._shuffleTimeout); + + if (seconds == null) { + let diff = -Math.floor(GLib.DateTime.new_now_local().difference(this.shuffledue)/1000000); + log('shuffle ('+this.shuffledue.format_iso8601()+') diff = '+diff); + if (diff > 0) { + seconds = diff; // if not specified, we should maintain the existing shuffle timeout (i.e. we just restored from saved state) + } + else if (this._settings.get_string('random-interval-mode') != 'custom') { + let random_mode = this._settings.get_string('random-interval-mode'); + seconds = Utils.seconds_until(random_mode); // else we shuffle at specified interval (midnight default) + log('shuffle mode = '+random_mode+' = '+seconds+' from now'); + } + else { + seconds = this._settings.get_int('random-interval'); // or whatever the user has specified (as a timer) + } + } + + this._shuffleTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._selectImage.bind(this, true)); + this.shuffledue = GLib.DateTime.new_now_local().add_seconds(seconds); + log('next shuffle in ' + seconds + ' seconds'); + } + + // auto export Bing data to JSON file if requested + _exportData() { + if (this._settings.get_boolean('always-export-bing-json')) { // save copy of current JSON + Utils.exportBingJSON(this._settings); + } + } + + // process Bing metadata + _parseData(data) { + try { + let parsed = JSON.parse(data); + let datamarket = parsed.market.mkt; + let prefmarket = this._settings.get_string('market'); + let newImages = Utils.mergeImageLists(this._settings, parsed.images); + + if (datamarket != prefmarket && prefmarket != 'auto') + log('WARNING: Bing returning market data for ' + datamarket + ' rather than selected ' + prefmarket); + + Utils.purgeImages(this._settings); // delete older images if enabled + //Utils.cleanupImageList(this._settings); // disabled, as old images should still be downloadble in theory + this._downloadAllImages(); // fetch missing images that are still available + Utils.populateImageListResolutions(this._settings); + + if (newImages.length > 0 && this._settings.get_boolean('revert-to-current-image')) { + // user wants to switch to the new image when it arrives + this._settings.set_string('selected-image', 'current'); + } + + if (this._settings.get_boolean('notify')) { + if (!this._settings.get_boolean('notify-only-latest')) { + // notify all new images + newImages.forEach((image) => { + log('New image to notify: ' + Utils.getImageTitle(image)); + this._createNotification(image); + }); + } + else { + // notify only the most recent image + let last = newImages.pop(); + if (last) { + log('New image to notify: ' + Utils.getImageTitle(last)); + this._createNotification(last); + } + } + } + + this._restartTimeoutFromLongDate(parsed.images[0].fullstartdate); // timing is set by Bing, and possibly varies by market + this._updatePending = false; + } + catch (error) { + log('_parseData() failed with error ' + error + ' @ '+error.lineNumber); + log(error.stack); + } + } + + _cleanUpImages() { + if (this._settings.get_boolean('delete-previous')) { + Utils.purgeImages(this._settings); + } + } + + _createNotification(image) { + // set notifications icon + let source = new MessageTray.Source('Bing Wallpaper', 'preferences-desktop-wallpaper-symbolic'); + Main.messageTray.add(source); + let msg = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.fullstartdate); + let details = Utils.getImageTitle(image); + let notification = new MessageTray.Notification(source, msg, details); + notification.setTransient(this._settings.get_boolean('transient')); + source.showNotification(notification); + } + + _shuffleImage() { + let image = null; + let imageList = Utils.getImageList(this._settings); + let filter = { 'faves': this._settings.get_boolean('random-mode-include-only-favourites'), + 'hidden': this._settings.get_boolean('random-mode-include-only-unhidden'), + 'min_height': this._settings.get_boolean('random-mode-include-only-uhd')?this._settings.get_int('min-uhd-height'):false + }; + let favImageList = Utils.getImageList(this._settings, filter); + + if (favImageList.length >= MINIMUM_SHUFFLE_IMAGES) { // we have the minimum images to shuffle, if not fall back to shuffle all iamges + imageList = favImageList; + } + else { + log('not enough filtered images available to shuffle'); + } + + // shuffle could fail for a number of reasons + try { + this.imageIndex = Utils.getRandomInt(imageList.length); + image = imageList[this.imageIndex]; + + log('shuffled to image '+image.urlbase); + + return image; + } + catch (e) { + log('shuffle failed '+e); + return null; + } + } + + _selectImage(force_shuffle = false) { + let imageList = Utils.getImageList(this._settings); + let image = null; + // special values, 'current' is most recent (default mode), 'random' picks one at random, anything else should be filename + + if (force_shuffle) { + image = this._shuffleImage(); + if (this._settings.get_boolean('random-mode-enabled')) + this._restartShuffleTimeout(); + } + + if (!image) { + if (this.selected_image == 'current') { + image = Utils.getCurrentImage(imageList); + this.imageIndex = Utils.getCurrentImageIndex(imageList); + } else { + image = Utils.inImageList(imageList, this.selected_image); + + if (!image) // if we didn't find it, try for current + image = Utils.getCurrentImage(imageList); + + if (image) + this.imageIndex = Utils.imageIndex(imageList, image.urlbase); + log('_selectImage: ' + this.selected_image + ' = ' + (image && image.urlbase) ? image.urlbase : 'not found'); + } + } + + if (!image) + return; // could force, image = imageList[0] or perhaps force refresh + + if (image.url != '') { + let resolution = Utils.getResolution(this._settings, image); + let BingWallpaperDir = Utils.getWallpaperDir(this._settings); + + // set current image details at extension scope + this.title = image.copyright.replace(/\s*[\(\(].*?[\)\)]\s*/g, ''); + this.explanation = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.startdate); + this.copyright = image.copyright.match(/[\(\(]([^)]+)[\)\)]/)[1].replace('\*\*', ''); // Japan locale uses () rather than () + this.longstartdate = image.fullstartdate; + this.imageinfolink = image.copyrightlink.replace(/^http:\/\//i, 'https://'); + this.imageURL = BingURL + image.urlbase + '_' + resolution + '.jpg'+'&qlt=100'; // generate image url for user's resolution @ high quality + this.filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution); + this.dimensions.width = image.width?image.width:null; + this.dimensions.height = image.height?image.height:null; + this.selected_image = Utils.getImageUrlBase(image); + this._settings.set_string('selected-image', this.selected_image); + + if (("favourite" in image) && image.favourite === true ) { + this.favourite_status = true; + } + else { + this.favourite_status = false; + } + + if (("hidden" in image) && image.hidden === true ) { + this.hidden_status = true; + } + else { + this.hidden_status = false; + } + + let file = Gio.file_new_for_path(this.filename); + let file_exists = file.query_exists(null); + let file_info = file_exists ? file.query_info ('*', Gio.FileQueryInfoFlags.NONE, null) : 0; + + if (!file_exists || file_info.get_size () == 0) { // file doesn't exist or is empty (probably due to a network error) + this._downloadImage(this.imageURL, file, true); + } + else { + this._setBackground(); + this._updatePending = false; + } + } + else { + this.title = _("No wallpaper available"); + this.explanation = _("No picture for today."); + this.filename = ""; + this._updatePending = false; + } + + this._setMenuText(); + this._storeState(); + } + + _imageURL(urlbase, resolution) { + return BingURL + urlbase + '_' + resolution + '.jpg'; + } + + _storeState() { + if (this.filename) { + let maxLongDate = Utils.getMaxLongDate(this._settings); // refresh date from most recent Bing image + let state = {maxlongdate: maxLongDate, title: this.title, explanation: this.explanation, copyright: this.copyright, + longstartdate: this.longstartdate, imageinfolink: this.imageinfolink, imageURL: this.imageURL, + filename: this.filename, favourite: this.favourite_status, width: this.dimensions.width, + height: this.dimensions.height, + shuffledue: (this.shuffledue.to_unix? this.shuffledue.to_unix():0) + }; + let stateJSON = JSON.stringify(state); + + log('Storing state as JSON: ' + stateJSON); + this._settings.set_string('state', stateJSON); + } + } + + _reStoreState() { + try { + // patch for relative paths, ensures that users running git version don't end up with broken state - see EGO review for version 38 https://extensions.gnome.org/review/30299 + this._settings.set_string('download-folder', this._settings.get_string('download-folder').replace('$HOME', '~')); + let stateJSON = this._settings.get_string('state'); + let state = JSON.parse(stateJSON); + let maxLongDate = null; + + log('restoring state...'); + maxLongDate = state.maxlongdate ? state.maxlongdate : null; + this.title = state.title; + this.explanation = state.explanation; + this.copyright = state.copyright; + this.longstartdate = state.longstartdate; + this.imageinfolink = state.imageinfolink; + this.imageURL = state.imageURL; + this.filename = state.filename; + this.dimensions.width = state.width; + this.dimensions.height = state.height; + this._selected_image = this._settings.get_string('selected-image'); + this.shuffledue = ("shuffledue" in state)? GLib.DateTime.new_from_unix_local(state.shuffledue) : 0; + this.favourite_status = ("favourite" in state && state.favourite === true); + // update menus and thumbnail + this._setMenuText(); + this._setBackground(); + + if (!maxLongDate) { + this._restartTimeout(60); + return; + } + + if (this._settings.get_boolean('random-mode-enabled')) { + log('random mode enabled, restarting random state'); + this._restartShuffleTimeoutFromDueDate(this.shuffledue); // FIXME: use state value + this._restartTimeoutFromLongDate(maxLongDate); + } + else { + this._restartTimeoutFromLongDate(maxLongDate); + } + + return; + } + catch (error) { + log('bad state - refreshing... error was ' + error); + } + this._restartTimeout(60); + } + + _downloadAllImages() { + // fetch recent undownloaded images + let imageList = Utils.getFetchableImageList(this._settings); + let BingWallpaperDir = Utils.getWallpaperDir(this._settings); + imageList.forEach( (image) => { + let resolution = Utils.getResolution(this._settings, image); + let filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution); + let url = this._imageURL(image.urlbase, resolution); + let file = Gio.file_new_for_path(filename); + this._downloadImage(url, file, false); + }); + } + + // download and process new image + // FIXME: improve error handling + _downloadImage(url, file, set_background) { + let BingWallpaperDir = Utils.getWallpaperDir(this._settings); + let dir = Gio.file_new_for_path(BingWallpaperDir); + if (!dir.query_exists(null)) { + dir.make_directory_with_parents(null); + } + log("Downloading " + url + " to " + file.get_uri()); + let request = Soup.Message.new('GET', url); + + // queue the http request + try { + if (Soup.MAJOR_VERSION >= 3) { + this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => { + this._processFileDownload(message, file, set_background); + }); + } + else { + this.httpSession.queue_message(request, (httpSession, message) => { + this._processFileDownload(message, file, set_background); + }); + } + } + catch (error) { + log('error sending libsoup message '+error); + } + } + + _processFileDownload(message, file, set_background) { + try { + let data = (Soup.MAJOR_VERSION >= 3) ? + this.httpSession.send_and_read_finish(message).get_data(): + message.response_body.flatten().get_as_bytes(); + + file.replace_contents_bytes_async( + data, + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null, + (file, res) => { + try { + file.replace_contents_finish(res); + if (set_background) + this._setBackground(); + log('Download successful'); + } + catch(e) { + log('Error writing file: ' + e); + } + } + ); + } + catch (error) { + log('Unable download image '+error); + } + } + + // open image in default image view + _openInSystemViewer() { + Utils.openInSystemViewer(this.filename); + } + + // open Bing image information page + _openImageInfoLink() { + if (this.imageinfolink) + Utils.openInSystemViewer(this.imageinfolink, false); + } + + stop() { + if (this._timeout) + GLib.source_remove(this._timeout); + + if (this._shuffleTimeout) + GLib.source_remove(this._shuffleTimeout); + + this._timeout = undefined; + this._shuffleTimeout = undefined; + this.menu.removeAll(); + blur._disable(); // disable blur (blur.js) override and cleanup + blur = null; + } +}); + +export default class BingWallpaperExtension extends Extension { + enable() { + bingWallpaperIndicator = new BingWallpaperIndicator(this); + Main.panel.addToStatusArea(IndicatorName, bingWallpaperIndicator); + } + disable() { + bingWallpaperIndicator.stop(); + bingWallpaperIndicator.destroy(); + bingWallpaperIndicator = null; + } +} + +function toFilename(wallpaperDir, startdate, imageURL, resolution) { + return wallpaperDir + startdate + '-' + imageURL.replace(/^.*[\\\/]/, '').replace('th?id=OHR.', '') + '_' + resolution + '.jpg'; +} + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/bing-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/bing-symbolic.svg new file mode 100644 index 0000000..7a6cc22 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/bing-symbolic.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/brick-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/brick-symbolic.svg new file mode 100644 index 0000000..eeb76b2 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/brick-symbolic.svg @@ -0,0 +1,117 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/checked-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/checked-symbolic.svg new file mode 100644 index 0000000..c7a0803 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/checked-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/fav-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/fav-symbolic.svg new file mode 100644 index 0000000..899bf9e --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/fav-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/game-die-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/game-die-symbolic.svg new file mode 100644 index 0000000..698416a --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/game-die-symbolic.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/high-frame-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/high-frame-symbolic.svg new file mode 100644 index 0000000..10e760f --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/high-frame-symbolic.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/low-frame-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/low-frame-symbolic.svg new file mode 100644 index 0000000..8a5dd79 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/low-frame-symbolic.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/mid-frame-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/mid-frame-symbolic.svg new file mode 100644 index 0000000..9e59f04 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/mid-frame-symbolic.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-empty-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-empty-symbolic.svg new file mode 100644 index 0000000..cea9587 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-empty-symbolic.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-full-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-full-symbolic.svg new file mode 100644 index 0000000..df732d8 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/trash-full-symbolic.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unchecked-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unchecked-symbolic.svg new file mode 100644 index 0000000..1ebd652 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unchecked-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unfav-symbolic.svg b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unfav-symbolic.svg new file mode 100644 index 0000000..6febe38 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/icons/unfav-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ar/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ar/LC_MESSAGES/BingWallpaper.mo new file mode 100755 index 0000000000000000000000000000000000000000..09116502514ce9f48946dc81a79909985b614aae GIT binary patch literal 5686 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 d-8pW4!d3exN!gZ{Rd331@Zs@ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ca/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ca/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..9f8249aa7a17291f1c61f2b89cf53b8208ebdbc2 GIT binary patch literal 2862 zcmbuA$!{D*6vhh@wn=~xAP}~^1jLx7XFO&Lc5F0W5+cR1WhVq7F4f&NJ(bjSHC5Gi zjDQOV4xB(jNQgs@;e=2mgdjH~_zyTCE<{M-!i57!NbtR$87~3Efs%WEy;Qw=%U91o z?A`f+z&M2K0bEaBCqxrGay@=9egpS`zk~OHdv*wM12_WS2{yn9a2k9Nd<%RC{21H? zeg)nSehZF(KZ3m1?wvx^!6tY+cm~`9X5e1%Ja`-UF35I20v`sy0C$7mgLi{hLH6%Y z@D6YvoMXEqAn(5j^7~gowwHr-K|qC8j{m$1$)4Dv;~)A;@uk3i7^RgO7q&z+>R9T0j0}a6g`7@J8?rko|fa zjKGWFt>AYc=i>^<@m&QuAHRUSu8!m)9RiP4{kY~l_!|$DxvDeQlJmlY>%fDh#WmzO zdGJ142k*zRvLBoSt{eNz`ryHJ<-zgt;CNXB2vra+)+LYoa3SqgzWDhdF3uI}Q5-pG z$*GQ0zDqvoskdjwhW@ZDs|9#HeA_gG{m&@DkYoK z$@X%a+KUUbbS$?$MZT+yA$2~e+-rP~*J#dYNo|*@JZR;#m}XmNq%nmo6gKNn=qj3( zYt*ss+OLI90(|lKW>PiyY-}Qp5;L|~Bi)mkqSH$!C|F92E@CTPDqzm@8y~Dwv^w0) zQ>?IyqEs2hL5W$_k);WwJ~!|+y;^>QX3f?MLMT7TRK!HL&C*L8WIbI z$_MEjBzs{6dSIVr8Ej7moxrlUrVLzKkF+FwCzb}1I%!wMa^e*1Y|azQ)jUL0l?so6 zVws%FoGtUMFHzNVP-V4zV=C9k3%Kn?7g6Fjgu;(Eo0$&XGHxWc*UVL?W1BH%MjLTO z8Ot%+5TfYmyyRjoI(rswN5|9XCFNM7<5XKxg>@mC_nA(k=`!=tDT~*O3(*p4&Xb~9 zNMW4DM#oyw=rhsi^VE83e600EYjkw9c0&3PopLhwhKq`oCuE+L5N32r$=;UdaY|%u z{`mad=H{)&Xl=&kI54oWR$!9=ubYK|3?>Kya9)MU)2EI`hd1Z(ULED4xjeBc0w1Tt zu@1GDqhl&puJ6R6RjCd%)_A(nsx6w*NfRBluII-oFRDjBIW|H4+sTof4o*_*v5A^E zG9wF;ICs+M@VZR&`Xz|pW={5GDyJZ0>Pq}&WuP+WPtajclI+D~B%PP;ycEkec1p~7 z9C4WkGWG2S2TcU_2x_VSM;oaVcZw~Ig!@SSUmHm^UEtIdDo9G}P*cdzL!jjCYX-;9 zk?enf_d%`5>MX5aYU)%&_S|RC!SIc{3~b=8Z;_-S7LJS@HZlgSLSviPN<{z!;8Ka` z$>3?|gN#wSYKpHbqajP3i$z42st$CiX#JDFb)vA*ijV@VkZzmFRQUuQLS<-3o{(*1 z2X%&^Hgmuo&nN2_cv^{LunXfWAt7P?0z?yQh)VXCY)>~jL$-byUSFd<1W^nV&N7fI zrEXL&I~J*gmjxU_`r)Pah0QhOhya}~xl~9^B?Df%qVT_DYwjS>q{Kh0Lv3`pLsc9p zdVLa*;;QMbUqIKBoJ+RV?`qY7=qG2mvuJM|7edL=5xGtN)5PPI7U6!r(ihc0O0air rIAR`WyMyhwC~o_<9DHmn%AzyKH1FsP$DFqnDU^to36K9@%k<(e^r#y> literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/cs/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/cs/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..7a93e54cb7d3d9fa2b87c56ecf6afcbea5f703d9 GIT binary patch literal 7358 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/fi_FI/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/fi_FI/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..fd1f3134ce4834663efb0b5635067490af9b6760 GIT binary patch literal 4884 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/hu/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/hu/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..beedea19cea437d150e2936c93a9f31f3a641d2c GIT binary patch literal 2904 zcmbW2%Wq`G6~-$80vPgO$diQRB(s=E==w1a7|dX$J@$Aej6KFP12eNh)%PCXF5l{^ zR^7VobSqMne;|>`Ya2^hq$N;@EO`+jMaq>o$s!0A?2uSRk;o?5us}+_)7`dv2H2$J z+rRd$$2sRar%u0j;{IPLjFWi2h3D*Nl$rsreik1X58bEKSHXwDuYm+U0GJU;-% z{wv@qumDA$Uw~rg58zYaKS0UD;}4JGxCn}$cRiADEZq1W&c|sj!++g zqWc3_Bz`^#ir>$G3*b#q&iy4Qx(~+x{|S`y{tkWz`~Va^9)v&wR|pc>>jZu?LWhu{PG zG_3+>s?qdSRE=$IqH+1*>oCwWv$Z$U9nVTyIcg{n`?(INxR}TWl@yk0QI$V7}R- zWotAwLe4H44n{4?SubmAJG#R#wn?$Yw4{5q>BGIV9g{}*Qew_#Jf5~b)t1#o-|3NQ z>zwK4##M@*QfoR*uR}IDch`HEDP?6DePG%?^rECD!#Y#OmL*uV#GAUZkxWU;F1H4X zC3Mr7h`6su9l693tYzv1?iyc}wnv%YcGl|*YmSdrOXIhPFJgHS*Mto0^q>-d*4M{vqxkBBHGa*TWfSK-1%iE(ckB?T@;CT?AbrpSE zN0Z{5(%ULZMU+r2m2YZ8yUe%8<~GJC$b+w3Hr^sHQ3?f0 zdANR93JRW;s)f+-8DpoMpP9)`Y*o$a)VF6GZ*KaTCe%-kTu*W--Q=ifRol2q61*P# z7JN;vWXVgTWX#jl26udj$!eLKELp7bGP&XLdi`3mfjW?kB}*uec{)9NdM=qgm&`s( zb7$tyoq1|*c6N5^sxD)4BWPDz$>coYMc;aQ0afcux^RKhmZ#>*rL8-+)}DTG<&M3x z@@((+t;JsRbjRL4^J3LJ^Fv2Jh8!h7eU_3W2l?A9gKE# zY>s5=XyH7ys;Sj0tINmX%}vivUGxsu7hd){2qQw5nT|zuJx8dkP;z|!=8a3q`QvpG z%_fIr*})S?J5T4ECQjW-R@iYEg}LrIGm%bDpPinYQWw^YOL3`#Da-oxSPx37-;CVm zewP>2<$BlFdwo<6wM96`vOWG?DP+IEa!wKR_|Lp*zqyL`#~(Qx;kDo zD9vQ(4YV*{;0Oo>(vwl5DX`>j_ zOZ5Q4Tl`G+IUM|P{i U#iQyyII^Yx?+%F{?v&O403*b50RR91 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/id/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/id/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..22046de1164679a934a6b868a425f8018254d2d3 GIT binary patch literal 2722 zcmbW2zi%8x6vqb$kT`@NA^ZvnOb8MVXwN@Ll*D!d=h#jx`5enX2oeqNcE@|;ot-&m zW@D~ER7eyN9a0LYk$}H|1|iWP8XA-?1r&&a3Q^D?zHjf&b^Z^8Vj}C&6F9$H0Ry<{|Jn z_yqVe$o9St^8VKbym-2(ks*BJ=jOiLy4zu zGs{kTwdqyp$dGnaRvDFD6isPW+;+Z@%6sRfOVi4$Of=iBilVtqI%=B_S2h|AF-t*Z z(pkCOTx-ha%GEh}*}786&{4)n^=?!)XpFaea^7f?O`j_pwenib@?O`-m@ah{4%?Km zqvRa*>q)I++^XZ9DRrz;=OqLUdlq z?cr%bwiQlMGE+veQ({hSQe~plLC)LUXeg7&4XY#kTZ=pEX+pkmr#XETb#s{Ok}*OjlO5?6GlYAv+QbV{&XEDpbkbx=z|q+bF56?77M#csb%BS}sIN+2jR>*ah%vO0w z)*ZT6t|qHU2o7B`hdh~*ljD;U$@rOM{Jflab?VI7S0={C$48bZ#AMx*4TeLCl}lvv z3R#=1D=PQ=o{~BnX)U(qcMqRvjE`J$7Pkp|^tw1C!s@ZEK@v<$gx?1hFKnzYBB{qsXUov!TB;wN!zV|h6@wZ zvj2Hue0oI8v?_|SGB937olyZ({1#Kea4WAVVFpa*I5UCW|4YEG}A0*~;d5 zCA+G`PMA{fC1#b+C?Ivwql!=$jzKZmEiTTe&OZvtwc)LnbxDC38tJZOWn)!{g}PwK zoXR%&J^9OX`&Gucf{a)rgFC_nWH7bBRi`ejX;Y!a9Ha>vM%$h)(To1UybSKTvWvv= zax{b6rDPLM?Ua-@ga{-T;k8G3No2D`{VPbW)rjH>b*r$)v>t04R9l&haSN)> z(3koLB!!f0gEl*rrOwv7$D@(CcS^8gdx=_X$lC{YHQB-y!j=GO> z4&O6EE_EdCHi^r9k%tiwD|dvLO99!fD#9nRUZ)OaG?Wq1scPXkc49ZCcb>>Tv;LCs z?*NV&+mXxShxoXipF-zjFjr cIRo;_rW*RMu>Q3jf?XC{RQ~_c1HYp98>LL&`v3p{ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/it/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/it/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..2f74f53373c738f21049b8aa8438189146ff8dc8 GIT binary patch literal 7186 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ja/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ja/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..df83f691a033bdac8442d64d9ac416dcf3017f28 GIT binary patch literal 5562 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 UA9 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ko/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/ko/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..19e433dd2cb16183ed36a1d7588da3413fcca385 GIT binary patch literal 5758 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 Q%_8a!zW=YTgNhgb0_GmyK>z>% literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/nl/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/nl/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..396f856e1fed1620698256e1b9cbfef608a4c486 GIT binary patch literal 2429 zcmbu9%WoV-5XPGjAS_9M@P4YpLpU=@$MjuYaSY$xFa>fNcG?YOsl)!k#q z;=+{!+_)e`0wKYFfO6u%Ey^t?B%~aOq2<}1XS%zpzN)Uif8y|$ z0^?OYFXK7-fDos^zd#;e9}?nO@Eh@Z>gN5N;n36T9SgS?-5^Phw4=PU3@@H_Ac@HY4~ zcn9SD_rW9J?;!rf-}qr2hw$?*cm#X|JP*DJ&6mIz@opb3*M9~+kM}RYS@3&Mg1>03}#KaQr?Bvi|cRuloo*34Q{y|63sI z{Q>0s{RFb#dm!)o6?`2${AlTK1Vl>3nI6xAtaloOYmtMHTZ|X*%QkqhCbr9i{jqP3 zj|bpBBDR-6l%Y5m z;@q;6o?4zlTZU$v(%ew`o|tL4Jc^lZrK7f4e?_%g6&F;XR5~k{XVz!r%<9UVT(GV~ zGPKDUNjD?epfTUrk@H3?IpZ_3QIpr=0^7Q-jOnT_dFfg*wn@&Z9ocez|7=$`V~)5m zXHx1-8`o5Z1hxbjoea@=lG}ZYK{g1MNTy_n0~d4DQn`sz2RUyuqoFd9SFMiFupU*U zfF)QI}yF8d3nYwLjoJz4~*R+`HeeK6{u<9-Hg?e#Gr&P$(Hq}jqK*f^MHsXO@ zBxgxC+lq{wRo**e#4=LXPqaW)dipsvkeaHUDt{dWB-NJEMm3O)6*%?mPUX>cRMbt_ z3C`q^>ky!?$^VHpWmDIYx}!1@YrUc`yKS-F^h9=OlUS!HRoL}bd}yfVX69XP)1D1p zg^FzAWW(X}>PoVPT5=hZIh1%(j@QQP zN$pHhJ1gs_C+qKw)@!v|Wl@EgYS4tg|@pFtgKzpNPQ6x&|elk|?eZ6sNClTudf**Ri`6`DETUT?*}!a-yMQ<-=rw zEcr5yRY$3ga=d!FTCc2{+$)n@biNZ1N4Izl)8kXJ{C9f9%9GQwerl>B&Mj~maT=_q zd>htfMhM5Eq4|u6>#EsCxYBib5&i-+iZlHIVoqZ{xnxn|wiTDWhYa;?^;d1r!lTo8xZSyJ;P@8O{tOBHMev i4F%kvg{aK4JX1z7EDCEb4{DC$;lzkM_y6G64ly{-iR)UnW&Vv9YX&2fKLP2vUb+4kNO2v+A*Aye@C0}UoB>ntVenIM1nhyN z^LLQ^`3EE)_V4S*cLF3oX21v(<@`G!P_KCJ!yi z=R}t`t-F3TQ;h&E9&0+5<+8R7p(Q(Ovo2R{kx0I|c7X@W8(L*`D_qR5ou?Tetdo3u zaGK|JiBlwxrIr*Fo0CnE>%f)g^Cr;>&P06EsDSv^g9y^&7Tha&=440N-0Lon?Y7ZY z#JFc?Yc^NT4z!+=q0ypXV-Wmmw%%|Od+yxF)*)Z` zo#Ts%x@e?{v$=`;*)1lK^At5(Y)}ZHS<0Yi2*R@#V#ap}nV%XRO;l**^=iYmM~!SY z?Pwi$M#Cthgo-s9mMLp1lT)f!o!x@)Xfcj1_R})OD{C^dE<{T{QE@bzCqCM+m|k6p z)=&|YyJ!xDGQ}sxCdQ+&XQQzf`1s`1#N>(bv9Ynr1>r-q;e_#;ayrFdfv!=I)~!uoONvv5m|BRXC|h3|L4qz;YVip`0?oqJGE#M!EKhI@bSju4S*P0&MH`B=O{vekY%?@2>M;?jH$Za#rcnIO zLyE405JX1{wt)xwGGmb-mcg0Apf_SS4i_8>f;W3t9lDU>Fd}e|r-EQ;I3`B>LLcVR zBvSj7k8G~jjT0q4b9Vp|CdV~uUZ@PL8v<^BTV^=%4~E%Z&L9>mGXT}^0cqK62- zg|cMp=~k}MLt>Ru5lF)pP^ToggC3++iWk0f<3}ih6vre+V!vt{jOiH=%-}Eb*t2Eh9ZjIGrSsDU)-X5SG??RUY63L XF&?5d&JYgGioOF{nEQX-DzJY6xC8+3 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pl/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pl/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..0883c9a522505c70584aa7c3d7d22c405ebf411e GIT binary patch literal 2632 zcmbu9ON>-S6oyL#AMG0y@c|S{G7z{uk3hr$NqU$8Cz)5~;TYnk`rexE>aMQ3sj5qR zTUQ8SUK@X@4Eu4PFDu@5kV);Md^e;2rQe@GeO4 z{S7_??nQ9q_XtSmFMwo!2YePZAnp4Ad<47!Vk^4|;=?|{*K6SC;KSh0;2+?x;6eKT zP)~)gk6nPgff zXIX_C8QRu0xpdAtZsRy}GGUdf%>t{mg$l~l20P2;GMf-yCfpi6S6Qm?%EJ64zhG=j za$lEPbNOzN#w*NMb3Ub&;1ySsCMf9?n;>7SLhDqd5{IqvP?vmCcS$`_@6M=VgfN zlx!k`Al3d`qD#^hQn5ExA`2~)NmMKlIhz&A1UxthO3tddE~Mt?1j?i}W?fSTZfxM< zl2BSywdBh1+-45ds-XaE4z(Cmj-mX0K}l=qlZd=b*3B%o9I^RSn!db!@#hh$qaZ~V zY)b?c!?U+K3shfWo@D@i}giW z$x21?3~_o^M_Tv}A@$?K!!;G^S+yM7*07OlYj(JbmCg_qfQ=9l8VCBwsd`G$td4{>&y=ymyP0iS*(5fTiMt|*6)21qG6v=hhjcWF` zHQ^8UL0q@(reM?BcGowq(Gke6BgvdYl_Fl8V)D4Yc^uyCR^DCLCg&AXQ&M|m6Vdem z%f*I-igCvp$yR*Uy@G_vBRu16qlgXOYKk{!vr0C6jH|>}aTU^OY+>*dcT#aGiMfi# zL^}7#m}t?zW7$g6S5w~3tC$~LEP329vO)R7ztj-7yH2e*XgapIeFkNeO{kecfAk*@Xg8i_ hI#pp{DkA1pjb2Ie-m;!->u$y<{|(9 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pt/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/pt/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..774abadfef1910abf8a8aa7e817f98cf7db53738 GIT binary patch literal 6216 zcmbuDTZ|-C8OIM71;+~)Z;OhaT@_f^?wOrs*=2Ti+1{PmVaeQ>o?#&|F{i7}bk}Zm zou*FJ3`3$ZYBW(|VhARhm`I2aR!J}fjXY>#W5NTzVR%6B(fDLYd@*?YeO1*xJv$pD zv9dM)syest{{4I3y7B5K6xV6)&v4&)rBV-p^H=kS>-qO7^$GA7;P1g#z)ynTd%sdV zs;5C2|2gcFlUUQ97H-o3a&w>X0Aow-#0QeaA1@PD4r@*&BS^p34 z)8KX2mgn99ewg>u72Xf-D+ez2HUgI`CCc&ifN6`n>~caL)&nx*oh6l>P1l zW!{6}jo>Eu5%4>p$oWH1cVe+5xp-M}Q-?

@-@+f6KM9I_GvLR-CMf%NKvYy;1)l)F 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 jRcmDk&CL|;xG0_KqUsd(=%+5wk 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 w^di7-f!rf-ksTDWWJc5S(c0Db=T|e+O99^ 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/tr/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/tr/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..18fa19bb4e8f6c473ecb88d2e2aa1fb54d0d86f4 GIT binary patch literal 7556 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/uk/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/uk/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..574e917b9b4d39215e006a1a4ea078e134682c9c GIT binary patch literal 9311 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 Pp{g4yp7M~+T{iy%pVqu! literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_CN/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_CN/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..b2e8a82b5a5615279123a86c0f31c224971b497f GIT binary patch literal 5184 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 OHS@;*{UHEnvi|^LN^w2_ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_TW/LC_MESSAGES/BingWallpaper.mo b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/locale/zh_TW/LC_MESSAGES/BingWallpaper.mo new file mode 100644 index 0000000000000000000000000000000000000000..8518d783fadc553b16748338f2a4b4df55da1d36 GIT binary patch literal 2654 zcmbW1S!@+m7{?C^F4qk~ToCby7)f+)TVyRuD6}AkmP(7LFJ$h_>AfSow({m=h9 zJ-^PH-onrp;F*W#^=XXdfvfMs58ChG)8HTAGvLho8Jhtv0%wD9a22>7d;#18&Idce zN5DbwdGIz!>+gVzz`sDsxo|pTuY#MvCqN0#1U~^O&M|Nn*atoieg{&XpTL*FyWpeX zY>1iza**^c11WAPNIEj$li)t^DeweH^Si-^z*`_fvD+a2*bn$w4*m>22*w^@Y%RQ* z1=2p=MknbqKyL5f!gZU);y(s>7*3r<54(lHOD{zc%k;6{-4xf5In?g7cKvmp5~ z3{u=5!52X<*FWRoXnYPx@m~SS&vhW_(sJAlQoJUR@|?@{e-BdpUvl38NP7MRX+N`L zjG;tqZjSRo@?$~n`+X30Fd$nm@|@z+T57bG^pMwduBg#@p@yTx9>YWRphmt=Zsb3< znXNZ@O*(0hv@dE@Z))TxHS&eDLptMl$S-QNZ`hyp`#JC>JmYP3ndG*pvW1i4j+PR7 z#uWNmR>Upg)<}C77q%^JE;TJ<3!N1uWX5GhHLBqnR&s1*JRWE3RY&MtTD+{NqKFrj zZ!PAVELknMlM<%M#fPr290TKtT3%urDlf8=!g38^8*Dwr+M$|eT1^WitKx1-@M5)= zS4sQc(X^qt(Bfds)J1OCl$vS^wn3(Axlyeq1>e4H3wI^gOe39;s;y5Ro6?Uo4$X92 zX$xL6ro!O~f$W0o!W6VwRxGO2jOlX2;U!kmG~k%Rw_ApbeOI`uD;!M0T*1?}*loy+ zW7cwA)>x)gbAmYQhxUuz0P^*eGfebz)^G z*;s)YPD##pDhce;)!I1qj&Eur5Zel!HYrpyuB=vFLqi-#ni-es@4zFC-q^O>xhs=3 ztU|MeuqMr86%d$EwOvVDW-L9bYPJ)#-i5Q8ou92W^-_f#22~DAVH5au-LzAfmzOl$ zR3;JEWObe;s;XpO0yC9y*-DZQsWN^%S+!whsFdZl`~W(Y&AReVWP2fxZ4+r}yGp5( zG<0QsCg~`Z65Zunm2Idv)kZ1CK`7*l^A{H=`Ae1jmAqhi;nHQV73AmV$F``Bt5n*m z<(O2nLjIniyj7dAc)4ov)oImQ`Nooh#p{yQCEH*G^D9bLx+ov<)uv=gjF0zz;=YYW_#sEWyQoil2avYrNq*t z4sQziiiF|D-c>dUOW2Xlaw&Af<%{D>;svpCGh?f!vQgUAP9e9_*-vLp!73iTuE}2& zW2^nn9{-1ZPSkT+# z-Kw8F`?tO4*~s9zd#?)~p1nD8voq-Ubn;zOwzaf`S7h%R|HL(~zTP|6HZpiU*gxQ% z9tgUQMAyhj|DkaB5|)k(9`xJJ`DdEF{<^zE9nom?>2$(*Ug*~Ud z!+TlS)qw2Y&C6ch(d-oyHeB_ZdcrG1!KL9Tqf-XF#-7pkF0bkE$nfVuOM8?DD!i_* z{g!?<`o*!ZyVu)0fLIKddbU8 z|MaN7jy!5=^gAzbHrjDHY&+^VHw0Y+UVAqieze)czFfkn bYxGDf4P>WJ8DJ2|{Pz9-`#$j+53s)hJ!gcg literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/metadata.json b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/metadata.json new file mode 100644 index 0000000..02d0da8 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/metadata.json @@ -0,0 +1,13 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Sync your wallpaper to today's Microsoft Bing image of the day (the image you see when you visit Bing.com).\n\n *Disclaimer*: this extension is unofficial and not affiliated with Bing or Microsoft in any way. Images are protected by copyright and are licensed only for use as wallpapers.\n\nFeatures:\n* UHD resolution wallpapers\n* Automatically fetches current Bing wallpaper of the day and sets as both lock screen and desktop wallpaper (user selectable on GNOME versions that support it)\n* Doesn't poll continuously - only once per day and on startup (schedules a refresh when Bing is due to update)\n * random mode (from previously downloaded wallpapers)\n *NEW: select/cycle wallpaper through previously downloaded images\n* Language support: English (en), German (de), Dutch (nl), Italian (it), Polish (pl), Chinese (zh_CN, zh_TW), French (fr), Portuguese (pt), Ukrainian (uk), Russian (ru_RU), Spanish (es), Korean (ko), Indonesian (id), Catalan (ca), Norwegian Bokm\u00e5l (nb) & Nynorsk (ni), Swedish (sv), Arabic (ar), Hungarian (hu) and Japanese (ja) - a HUGE thanks to the translators\n\nThis extension was forked from the NASA APOD extension by Elinvention (https://github.com/Elinvention) and inspired by Bing Desktop Wallpaper Changer by Utkarsh Gupta (https://github.com/UtkarshGpta).\n\nAlways restart GNOME after manually updating extensions. Please report bugs to the GitHub page below:", + "gettext-domain": "BingWallpaper", + "name": "Bing Wallpaper", + "settings-schema": "org.gnome.shell.extensions.bingwallpaper", + "shell-version": [ + "45" + ], + "url": "https://github.com/neffo/bing-wallpaper-gnome-extension", + "uuid": "BingWallpaper@ineffable-gmail.com", + "version": 48 +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/prefs.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/prefs.js new file mode 100644 index 0000000..4986c7a --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/prefs.js @@ -0,0 +1,247 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. +// Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod + +import Gtk from 'gi://Gtk'; +import Gdk from 'gi://Gdk'; +import Gio from 'gi://Gio'; +import Soup from 'gi://Soup'; +import Adw from 'gi://Adw'; +import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; +import * as Config from 'resource:///org/gnome/Shell/Extensions/js/misc/config.js'; +import * as Utils from './utils.js'; +import Carousel from './carousel.js'; + +const BingImageURL = Utils.BingImageURL; + +var DESKTOP_SCHEMA = 'org.gnome.desktop.background'; + +var PREFS_DEFAULT_WIDTH = 950; +var PREFS_DEFAULT_HEIGHT = 950; + +export default class BingWallpaperExtensionPreferences extends ExtensionPreferences { + fillPreferencesWindow(window) { + // formally globals + let settings = this.getSettings(Utils.BING_SCHEMA); + let desktop_settings = this.getSettings(Utils.DESKTOP_SCHEMA); + + window.set_default_size(PREFS_DEFAULT_WIDTH, PREFS_DEFAULT_HEIGHT); + + let icon_image = null; + let provider = new Gtk.CssProvider(); + + let carousel = null; + let httpSession = null; + + let log = (msg) => { // avoids need for globals + if (settings.get_boolean('debug-logging')) + console.log("BingWallpaper extension: " + msg); // disable to keep the noise down in journal + } + + // Prepare labels and controls + let buildable = new Gtk.Builder(); + // GTK4 removes some properties, and builder breaks when it sees them + buildable.add_from_file( this.dir.get_path() + '/ui/Settings4.ui' ); + provider.load_from_path(this.dir.get_path() + '/ui/prefs.css'); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + + let box = buildable.get_object('prefs_widget'); + + // fix size of prefs window in GNOME shell 40+ (but super racy, so is unreliable) + + buildable.get_object('extension_version').set_text(this.metadata.version.toString()); + buildable.get_object('extension_name').set_text(this.metadata.name.toString()); + + // assign variables to UI objects we've loaded + let hideSwitch = buildable.get_object('hide'); + let iconEntry = buildable.get_object('icon'); + let notifySwitch = buildable.get_object('notify'); + let bgSwitch = buildable.get_object('background'); + let styleEntry = buildable.get_object('background_style'); + let fileChooserBtn = buildable.get_object('download_folder'); + let fileChooser = buildable.get_object('file_chooser'); // this should only exist on Gtk4 + let folderOpenBtn = buildable.get_object('button_open_download_folder'); + let marketEntry = buildable.get_object('market'); + let resolutionEntry = buildable.get_object('resolution'); + let historyEntry = buildable.get_object('history'); + icon_image = buildable.get_object('icon_image'); + let overrideSwitch = buildable.get_object('lockscreen_override'); + let strengthEntry = buildable.get_object('entry_strength'); + let brightnessEntry = buildable.get_object('entry_brightness'); + let debugSwitch = buildable.get_object('debug_switch'); + let revertSwitch = buildable.get_object('revert_switch'); + let unsafeSwitch = buildable.get_object('unsafe_switch'); + let randomIntervalEntry = buildable.get_object('entry_random_interval'); + let change_log = buildable.get_object('change_log'); + let buttonGDMdefault = buildable.get_object('button_default_gnome'); + let buttonnoblur = buildable.get_object('button_no_blur'); + let buttonslightblur = buildable.get_object('button_slight_blur'); + let buttonImportData = buildable.get_object('button_json_import'); + let buttonExportData = buildable.get_object('button_json_export'); + let switchAlwaysExport = buildable.get_object('always_export_switch'); + let switchEnableShuffle = buildable.get_object('shuffle_enabled_switch'); + let entryShuffleMode = buildable.get_object('shuffle_mode_combo'); + let carouselFlowBox = (Gtk.get_major_version() == 4) ? buildable.get_object('carouselFlowBox'): null; + + try { + httpSession = new Soup.Session(); + httpSession.user_agent = 'User-Agent: Mozilla/5.0 (X11; GNOME Shell/' + Config.PACKAGE_VERSION + '; Linux x86_64; +https://github.com/neffo/bing-wallpaper-gnome-extension ) BingWallpaper Gnome Extension/' + this.metadata.version; + } + catch (e) { + log("Error creating httpSession: " + e); + } + + // check that these are valid (can be edited through dconf-editor) + Utils.validate_resolution(settings); + Utils.validate_icon(settings, this.path, icon_image); + + // Indicator & notifications + settings.bind('hide', hideSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('notify', notifySwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + + // add markets to dropdown list (aka a GtkComboText) + Utils.icon_list.forEach((iconname, index) => { + iconEntry.append(iconname, iconname); + }); + + // user selectable indicator icons + settings.bind('icon-name', iconEntry, 'active_id', Gio.SettingsBindFlags.DEFAULT); + settings.connect('changed::icon-name', () => { + Utils.validate_icon(settings, this.path, icon_image); + }); + iconEntry.set_active_id(settings.get_string('icon-name')); + + // connect switches to settings changes + settings.bind('set-background', bgSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('debug-logging', debugSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('revert-to-current-image', revertSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('override-unsafe-wayland', unsafeSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('random-interval', randomIntervalEntry, 'value', Gio.SettingsBindFlags.DEFAULT); + settings.bind('always-export-bing-json', switchAlwaysExport, 'active', Gio.SettingsBindFlags.DEFAULT); + + // button opens Nautilus at our image folder + folderOpenBtn.connect('clicked', (widget) => { + Utils.openImageFolder(settings); + }); + + // we populate the tab (gtk4+, gnome 40+), this was previously a button to open a new window in gtk3 + carousel = new Carousel(settings, null, null, carouselFlowBox, this.dir.get_path()); // auto load carousel + + // this is intended for migrating image folders between computers (or even sharing) or backups + // we export the Bing JSON data to the image directory, so this folder becomes portable + buttonImportData.connect('clicked', () => { + Utils.importBingJSON(settings); + }); + buttonExportData.connect('clicked', () => { + Utils.exportBingJSON(settings); + }); + + //download folder + fileChooserBtn.set_label(Utils.getWallpaperDir(settings)); + + fileChooserBtn.connect('clicked', (widget) => { + let parent = widget.get_root(); + let curWallpaperDir = Gio.File.new_for_path(Utils.getWallpaperDir(settings)); + fileChooser.set_current_folder(curWallpaperDir.get_parent()); + fileChooser.set_action(Gtk.FileChooserAction.SELECT_FOLDER); + fileChooser.set_transient_for(parent); + fileChooser.set_accept_label(_('Select folder')); + fileChooser.show(); + }); + + fileChooser.connect('response', (widget, response) => { + if (response !== Gtk.ResponseType.ACCEPT) { + return; + } + let fileURI = fileChooser.get_file().get_path().replace('file://', ''); + fileChooserBtn.set_label(fileURI); + Utils.moveImagesToNewFolder(settings, Utils.getWallpaperDir(settings), fileURI); + Utils.setWallpaperDir(settings, fileURI); + }); + + // in Gtk 4 instead we use a DropDown, but we need to treat it a bit special + let market_grid = buildable.get_object('market_grid'); + marketEntry = Gtk.DropDown.new_from_strings(Utils.marketName); + marketEntry.set_selected(Utils.markets.indexOf(settings.get_string('market'))); + market_grid.attach(marketEntry, 1, 0, 1, 2); + marketEntry.connect('notify::selected-item', () => { + let id = marketEntry.get_selected(); + settings.set_string('market', Utils.markets[id]); + }); + + settings.connect('changed::market', () => { + marketEntry.set_selected(Utils.markets.indexOf(settings.get_string('market'))); + }); + + settings.connect('changed::download-folder', () => { + fileChooserBtn.set_label(Utils.getWallpaperDir(settings)); + }); + + + // Resolution + Utils.resolutions.forEach((res) => { // add res to dropdown list (aka a GtkComboText) + resolutionEntry.append(res, res); + }); + + settings.bind('resolution', resolutionEntry, 'active_id', Gio.SettingsBindFlags.DEFAULT); + + settings.connect('changed::resolution', () => { + Utils.validate_resolution(settings); + }); + + // shuffle modes + settings.bind('random-mode-enabled', switchEnableShuffle, 'active', Gio.SettingsBindFlags.DEFAULT); + Utils.randomIntervals.forEach((x) => { + entryShuffleMode.append(x.value, _(x.title)); + }); + settings.bind('random-interval-mode', entryShuffleMode, 'active_id', Gio.SettingsBindFlags.DEFAULT); + + // selected image can no longer be changed through a dropdown (didn't scale) + settings.bind('selected-image', historyEntry, 'label', Gio.SettingsBindFlags.DEFAULT); + settings.connect('changed::selected-image', () => { + Utils.validate_imagename(settings); + }); + + // background styles (e.g. zoom or span) + Utils.backgroundStyle.forEach((style) => { + styleEntry.append(style, style); + }); + desktop_settings.bind('picture-options', styleEntry, 'active_id', Gio.SettingsBindFlags.DEFAULT); + + // GDM3 lockscreen blur override + settings.bind('override-lockscreen-blur', overrideSwitch, 'active', Gio.SettingsBindFlags.DEFAULT); + settings.bind('lockscreen-blur-strength', strengthEntry, 'value', Gio.SettingsBindFlags.DEFAULT); + settings.bind('lockscreen-blur-brightness', brightnessEntry, 'value', Gio.SettingsBindFlags.DEFAULT); + + // add a couple of preset buttons + buttonGDMdefault.connect('clicked', (widget) => { + Utils.set_blur_preset(settings, Utils.PRESET_GNOME_DEFAULT); + }); + buttonnoblur.connect('clicked', (widget) => { + Utils.set_blur_preset(settings, Utils.PRESET_NO_BLUR); + }); + buttonslightblur.connect('clicked', (widget) => { + Utils.set_blur_preset(settings, Utils.PRESET_SLIGHT_BLUR); + }); + + // fetch + if (httpSession) + Utils.fetch_change_log(this.metadata.version.toString(), change_log, httpSession); + + const page = new Adw.PreferencesPage(); + const group = new Adw.PreferencesGroup(); + + group.add(box); + page.add(group); + + window.add(page); + } +} diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/gschemas.compiled b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..77f335cdc4d35f582d719ad2abaf7a9ab30ff7d5 GIT binary patch literal 2432 zcmZ`*U1%It7`@RXCaq~VX=;<$*2Eath2Cs!iv&zjk=D?rL|-(;7ISy!W_Qf&%rZaO ztlAQ)VC{qYQmlolpcPRn3Ppl6+804EmHN;Z5iEreQIKE^t*GcZcXl^6*nyLC_RO7c z?)~m}zsX}yD&JOaC@NP6cr@0@+o~qGVWSZL;EL}Jn=yyr|FXB~1cy;4#gKRje5ziE zI%d&o8Zjw80DlTJ z?o$c+)D7@wz+VD~M!KJ;Pu&dvYw-8LY}4q^^r_q7FM^kW4?7y`=~L5BG+=JPGsA`b{~D} z4e)2cv%p(*J#+M_IsP*E3h>4K-;L0xrvDT8cVOVcqiyu5*TZjW6rvY6KQh0cK6MZL zA@Bj9T1V7E42#FXKLE!TmdrG4{D^r@M*Pr%;*V&`d1pE?2mDtI2)dhB(x z@6?U(7s1QG#Un@8@_bOU9s~+h0)LD?c#d(XTi`zo-UD>*oW4Pyn(G<^YoPhJ!4~?| zj9&zofw}8TCHmB?tK;C;fb-`D&Aw1GZ|{K50$UC~G|%zW%)>?SSHPL4PWIBLW}M65 zE5P25GF|kkH^aXH{u{XT$onVhQ}@E}#-Zy28uhMU=~MH)1K{1jkJsLKfj)IT{2}lG zp!;h3tMsXvhcU1Q#;*T$jXpK~DeyGl>%eoO5X}NNaj7Wu&?bUZ5UNlM@$_Ny%g%bP zbX7sKp~#?I$as#OHMc@v1$k*{M~6C)c0uKIz(~nV%2%%C6{PKk+MiU8EO-_|n#)qQ zQ;uJh_ewJBMQ(_J@$)jYL#H|talA?G`?jTJGy`9-swaU+0wom@0k)*vHX=u>QE8-wuxVUKV{lX?u~`e7M|yjg1Rs=*dc2RuW~_ z;dt4JAnR-G%8V2FGUIcfTpa{r2O=;}yy9)B?bGU~f)E+cg?TfU>%PTvzAic{%j!Wn z7L-3>9^giyxI^D_P`xbkz04&RhgDZ z<0G9mAL+FDNT*jm(&?K%(rF=>63kJC`KXYoq|@X}IjHZziKw3?&%Y*0P9cpn#~ z<2tXM<5i<7=b07FJjyuzmHF3JwkpMDMTn21j{hr6d7JrE3f!ESn=3XSpOQ-4TaFu7 zlV0T8II$*wmRE8ePg!!@b1d`!3Ibq%Q2y0!5RcQDD2Msya_B~8avYaQRVz?hNK@>y J?y+~Ne*qmT_hA44 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/org.gnome.shell.extensions.bingwallpaper.gschema.xml b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/org.gnome.shell.extensions.bingwallpaper.gschema.xml new file mode 100644 index 0000000..c457c1d --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/schemas/org.gnome.shell.extensions.bingwallpaper.gschema.xml @@ -0,0 +1,249 @@ + + + + + + false +

Hide indicator + Hide indicator icon from status area + + + + false + Send a notifications + Send a notification with explanation when a new image is available + + + + true + Send a notifications only on most recent image + Notify only the most recent image, in combination with 'notify' setting + + + + true + Use transient notifications (auto dismiss) + + + + + true + Set background + Set background to Bing's background + + + + false + Set lock screen + Set lock screen to Bing's background + + + + "" + Where to download pictures. + + + + + "auto" + Default Market for Bing + + + + + "auto" + Screen Size + Valid sizes are '800x600', '1024x768', '1280x720', '1280x768', '1366x768', '1920x1080', '1920x1200' and 'UHD' + + + + false + Log messages to systemd journal + Unlikely to be useful for ordinary users, but helpful for debugging + + + + false + Delete previous wallpapers + Save disk space + + + + 8 + Number of days to keep wallpapers + Delete wallpapers after this number of days + + + + "" + Replace or remove Bing watermark on high-res images + Set to "none", "Gnome" or "Ubuntu" + + + + "bing-symbolic" + Icon Name + Icon filename to use for extension applet + + + + false + Ignore Bing permitted for use as wallpaper flag + Used when wallpaper is animated, or not licensed for wallpaper use + + + + false + Override GNOME lockscreen (gdm3) wallpaper blur + Applies to GNOME 3.36 and above only + + + + + 2 + GNOME lockscreen (gdm3) wallpaper blur strength + Degree of fuzziness + + + + + 50 + GNOME lockscreen (gdm3) wallpaper blur Brightness + Used to darken the wallpaper to improve visibility + + + + "current" + Use older Bing image, select by filename + + + + + true + Revert to showing the current image when a new one arrives + This setting may best reflect what users want from selectable or random images + + + + "[]" + Store state in JSON format so we can restore details immediately on boot + + + + + "[]" + Store Bing image details for rollback + + + + + "daily" + Set to either 'daily', 'hourly', 'weekly', 'custom' + Daily is midnight, hourly is on the hour, weekly is midnight on Sunday, or custom as defined by the 'random-interval' + + + + + 3600 + How frequently to change the wallpaper when on random mode + + + + + false + Only pick from favourites in random mode + + + + + false + Pick a random image at random-interval + + + + + false + Only pick from UHD in random mode + + + + + true + Only pick from unhidden in random mode + + + + + true + Override safe defaults for Wayland desktop + Enables features which may or may not be unstable or untested in all cases + + + + true + Create thumbnails to speed up image gallery loading + Speeds up subsequent loads, but requires some additional disk space + + + + + 2160 + Minimum image height to be considered UHD + + + + + false + Save backup copy of Bing JSON to wallpaper directory + Enables migration to new install or restoration from back up + + + + false + Include current index and total image count in image title + + + + + 32 + Size of icons for controls in popup menu + + + + false + Trash deletes images or just marks as bad + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/stylesheet.css b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/stylesheet.css new file mode 100644 index 0000000..7dc7997 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/stylesheet.css @@ -0,0 +1,2 @@ +.wp-thumbnail-image:focus, .wp-thumbnail-image:hover { + background-color: rgba(247, 247, 247, 0.1) !important; } \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/thumbnail.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/thumbnail.js new file mode 100644 index 0000000..58bdb55 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/thumbnail.js @@ -0,0 +1,29 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. + +import Gio from 'gi://Gio'; +import GdkPixbuf from 'gi://GdkPixbuf'; + +const THUMBNAIL_WIDTH = 480; +const THUMBNAIL_HEIGHT = 270; + +export default class Thumbnail { + constructor(filePath, scale = 1.0) { + if (!filePath) { + throw new Error(`need argument ${filePath}`); + } + try { + let w = Math.round(THUMBNAIL_WIDTH * scale); + let h = Math.round(THUMBNAIL_HEIGHT * scale); + this.pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filePath, w, h); + this.srcFile = Gio.File.new_for_path(filePath); + } catch (err) { + log('Unable to create thumbnail for corrupt or incomplete file: ' + filePath + ' err: ' + err); + } + } +}; diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/Settings4.ui b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/Settings4.ui new file mode 100644 index 0000000..073a3bf --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/Settings4.ui @@ -0,0 +1,1451 @@ + + + + + + 50 + 1 + 10 + + + 100 + 5 + 10 + + + 7 + 1 + 2 + + + 300 + 604800 + 300 + 3600 + + + Bing Wallpaper pictures folder + 0 + select-folder + 1 + + + + 120 + + + 0 + 0 + 0 + 500 + + + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + 0 + + + 0 + + + 0 + none + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Hide the indicator + 1 + + 0 + 0 + + + + + + end + 1 + + 1 + 0 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Indicator icon + 1 + + 0 + 0 + 2 + + + + + + 0 + end + + 1 + 1 + 1 + + + + + + 32 + 32 + 0 + end + 1 + ../icons/bing-symbolic.svg + icon_image_black_bg + + 1 + 0 + 1 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Enable desktop notifications + 1 + + 0 + 0 + + + + + + end + 1 + + 1 + 0 + + + + + + + + + + + + + + + + + 0 + + + 0 + none + + + + + 0 + 12 + 12 + 12 + 12 + + + 0 + Set background image + + 0 + 0 + + + + + + end + 1 + + 1 + 0 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Background style option + + 0 + 0 + + + + + + + 1 + 0 + 1 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + Download folder + + 0 + 0 + + + + + + True + False + end + false + Bing Wallpaper pictures folder + + 1 + 0 + + + + + + Open folder + True + True + + 2 + 0 + + + + + + + + + + + + + + + + + 0 + + + 0 + none + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Selected image + + 0 + 0 + + + + + + + 1 + 0 + 1 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Shuffle enabled + + 0 + 0 + + + + + + end + 1 + + 1 + 0 + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Shuffle mode + + 0 + 0 + + + + + + + 1 + 0 + 1 + + + + + + + + + + + + + + + + + 0 + + + 0 + none + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Bing locale + 1 + + 0 + 0 + + + + + + 0 + start + 0 + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + 0 + Settings + + + + + + + 1 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 0 + + + 0 + none + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + + 1 + 0 + + + + + + 0 + start + 1 + Dynamically switches blur on GDM3 lock screen + + + 0 + 1 + + + + + + 0 + start + 1 + Enable dynamic lockscreen blur + 1 + + 0 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Blur can improve readability + + + 0 + 1 + + + + + + 0 + start + 1 + Background blur intensity + + 0 + 0 + + + + + + adjustment_blur + 1 + + 1 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Can improve contrast of login prompt + + + 0 + 1 + + + + + + 0 + start + 1 + Background brightness + + 0 + 0 + + + + + + adjustment_brightness + 1 + + 1 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Presets + 1 + + 0 + 0 + + + + + + No blur, slight dim + 1 + + 1 + 3 + + + + + + GNOME default + 1 + + 1 + 2 + + + + + + Slight blur, slight dim + 1 + + 1 + 4 + + + + + + + + + + + + + + + + + + + + + + 0 + Lock Screen + + + + + + + 2 + + + 0 + 12 + 12 + 12 + 12 + vertical + 12 + + + 0 + 0 + + + 0 + none + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + end + + 1 + 0 + + + + + + 0 + start + 1 + Enable logging to system journal + + + 0 + 1 + + + + + + 0 + start + 1 + Debug logging + 1 + + 0 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Switch to new images when available (unless on random mode) + + + 0 + 1 + + + + + + 0 + start + 1 + Always show new images + 1 + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Some newer features may be unstable on Wayland + + + 0 + 1 + + + + + + 0 + start + 1 + Enable all features on Wayland + 1 + + 0 + 0 + + + + + + end + + 1 + 0 + + + + + + + + + + + + + 0 + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Screen resolution + 1 + + 0 + 0 + + + + + + 0 + start + Override automatic resolution selection + + + 0 + 1 + + + + + + + 1 + 0 + 2 + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Manually adjust random interval (seconds) + + + 0 + 1 + + + + + + 0 + start + 1 + Random interval + + 0 + 0 + + + + + + adjustment_random_interval + 1 + + 1 + 0 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Import Bing Wallpaper data + 1 + + 0 + 0 + + + + + + 0 + start + 1 + Import previously exported JSON data from wallpaper directory + + + 0 + 1 + + + + + + Import + 1 + + 1 + 1 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Export Bing Wallpaper data + 1 + + 0 + 0 + + + + + + 0 + start + 1 + Export JSON data to wallpaper dir for backup or data migration + + + 0 + 1 + + + + + + Export + 1 + + 1 + 1 + + + + + + + + + + + + + + + 0 + 12 + 12 + 12 + 12 + 32 + + + 0 + start + 1 + Always export Bing data + 1 + + 0 + 0 + + + + + + 0 + start + 1 + Export Bing JSON whenever data changes + + + + 0 + 1 + + + + + + end + + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + 0 + Debug options + + + + + + + 3 + + + 0 + vertical + + + + 0 + 500 + 0 + 0 + + + 0 + center + 1 + 2 + 2 + 2 + 2 + + + + + + + + + 0 + Gallery + + + + + + + 4 + + + 0 + + + 1 + 0 + vertical + 5 + + + + 0 + Bing Wallpaper + + + + + + + + 0 + New wallpaper images everyday from Bing + center + 1 + + + + + 0 + center + + + + 0 + end + GNOME shell extension version + + + + + 0 + start + ... + + + + + + + https://github.com/neffo/bing-wallpaper-gnome-extension + 0 + 1 + center + https://github.com/neffo/bing-wallpaper-gnome-extension + + + + + 0 + center + vertical + 5 + + + 0 + Maintained by Michael Carroll + + + + + 0 + ineffable@gmail.com + 1 + + + + + 0 + 36 + Show your support to the author on <a href="https://flattr.com/@neffo">Flattr</a> or <a href="https://github.com/sponsors/neffo">Github Sponsors</a>. + 1 + + + + + 0 + 36 + + 1 + + Changes since last version + 1 + + + + + 0 + 36 + Based on NASA APOD GNOME shell extension by Elia Argentieri + center + 1 + + + + + + + + + + 0 + 0 + <span size="small">This program comes with ABSOLUTELY NO WARRANTY. +See the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU General Public License, version 3 or later</a> for details.</span> + 1 + center + 1 + + + + + + + 0 + About + + + + + + diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/carousel4.ui b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/carousel4.ui new file mode 100644 index 0000000..4be8a98 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/carousel4.ui @@ -0,0 +1,209 @@ + + + + + + + never + + + 0 + 0 + 500 + + + 0 + center + 1 + + 2 + 10 + 4 + + + + + + + 0 + + + 0 + 0 + 5 + + + + 320 + 180 + 0 + 1 + 1 + + + + + 0 + vertical + 1 + center + + + Favorite + checkbox + + 1 + 0 + center + + + + + Favorite + checkbox-checked + + 1 + 0 + center + + + + + Apply + preferences-desktop-wallpaper + 1 + 0 + center + + + + + View + view-reveal + 1 + 0 + center + + + + + Info + preferences-system-details + 1 + 0 + center + + + + + Delete + edit-delete + 1 + 0 + center + + + + + + + + + 0 + + + 0 + center + + + 320 + 120 + 0 + media-playlist-shuffle + 2 + + 0 + 0 + 2 + + + + + + 0 + <image name here> + + 0 + 1 + 2 + + + + + + Set random mode + 1 + + 0 + 2 + 2 + + + + + + + + 0 + + + 0 + center + + + 320 + 120 + 0 + preferences-desktop-wallpaper-symbolic + 3 + + 0 + 0 + + + + + + Load image gallery + 1 + + 0 + 1 + + + + + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/prefs.css b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/prefs.css new file mode 100644 index 0000000..78f23dc --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/ui/prefs.css @@ -0,0 +1,4 @@ +.icon_image_black_bg +{ + background-color: rgba(0, 0, 0, 1); +} diff --git a/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/utils.js b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/utils.js new file mode 100644 index 0000000..6f2ff22 --- /dev/null +++ b/gnome/.local/share/extensions/BingWallpaper@ineffable-gmail.com/utils.js @@ -0,0 +1,623 @@ +// Bing Wallpaper GNOME extension +// Copyright (C) 2017-2023 Michael Carroll +// This extension is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// See the GNU General Public License, version 3 or later for details. +// Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Soup from 'gi://Soup'; +import GdkPixbuf from 'gi://GdkPixbuf'; + +export var PRESET_GNOME_DEFAULT = { blur: 60, dim: 55 }; // as at GNOME 40 +export var PRESET_NO_BLUR = { blur: 0, dim: 60 }; +export var PRESET_SLIGHT_BLUR = { blur: 2, dim: 60 }; + +export var BING_SCHEMA = 'org.gnome.shell.extensions.bingwallpaper'; +export var DESKTOP_SCHEMA = 'org.gnome.desktop.background'; + +var vertical_blur = null; +var horizontal_blur = null; + +let gitreleaseurl = 'https://api.github.com/repos/neffo/bing-wallpaper-gnome-extension/releases/tags/'; +let debug = false; + +export var icon_list = ['bing-symbolic', 'brick-symbolic', 'high-frame-symbolic', 'mid-frame-symbolic', 'low-frame-symbolic']; +export var resolutions = ['auto', 'UHD', '1920x1200', '1920x1080', '1366x768', '1280x720', '1024x768', '800x600']; +export var markets = ['auto', 'ar-XA', 'da-DK', 'de-AT', 'de-CH', 'de-DE', 'en-AU', 'en-CA', 'en-GB', + 'en-ID', 'en-IE', 'en-IN', 'en-MY', 'en-NZ', 'en-PH', 'en-SG', 'en-US', 'en-WW', 'en-XA', 'en-ZA', 'es-AR', + 'es-CL', 'es-ES', 'es-MX', 'es-US', 'es-XL', 'et-EE', 'fi-FI', 'fr-BE', 'fr-CA', 'fr-CH', 'fr-FR', + 'he-IL', 'hr-HR', 'hu-HU', 'it-IT', 'ja-JP', 'ko-KR', 'lt-LT', 'lv-LV', 'nb-NO', 'nl-BE', 'nl-NL', + 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'ru-RU', 'sk-SK', 'sl-SL', 'sv-SE', 'th-TH', 'tr-TR', 'uk-UA', + 'zh-CN', 'zh-HK', 'zh-TW']; +export var marketName = [ + 'auto', '(شبه الجزيرة العربية‎) العربية', 'dansk (Danmark)', 'Deutsch (Österreich)', + 'Deutsch (Schweiz)', 'Deutsch (Deutschland)', 'English (Australia)', 'English (Canada)', + 'English (United Kingdom)', 'English (Indonesia)', 'English (Ireland)', 'English (India)', 'English (Malaysia)', + 'English (New Zealand)', 'English (Philippines)', 'English (Singapore)', 'English (United States)', + 'English (International)', 'English (Arabia)', 'English (South Africa)', 'español (Argentina)', 'español (Chile)', + 'español (España)', 'español (México)', 'español (Estados Unidos)', 'español (Latinoamérica)', 'eesti (Eesti)', + 'suomi (Suomi)', 'français (Belgique)', 'français (Canada)', 'français (Suisse)', 'français (France)', + '(עברית (ישראל', 'hrvatski (Hrvatska)', 'magyar (Magyarország)', 'italiano (Italia)', '日本語 (日本)', '한국어(대한민국)', + 'lietuvių (Lietuva)', 'latviešu (Latvija)', 'norsk bokmål (Norge)', 'Nederlands (België)', 'Nederlands (Nederland)', + 'polski (Polska)', 'português (Brasil)', 'português (Portugal)', 'română (România)', 'русский (Россия)', + 'slovenčina (Slovensko)', 'slovenščina (Slovenija)', 'svenska (Sverige)', 'ไทย (ไทย)', 'Türkçe (Türkiye)', + 'українська (Україна)', '中文(中国)', '中文(中國香港特別行政區)', '中文(台灣)' +]; + +export var backgroundStyle = ['none', 'wallpaper', 'centered', 'scaled', 'stretched', 'zoom', 'spanned']; // this may change in the future + +export var randomIntervals = [ {value: 'hourly', title: ('on the hour')}, + {value: 'daily', title: ('every day at midnight')}, + {value: 'weekly', title: ('every Sunday at midnight')} ]; + +export var BingImageURL = 'https://www.bing.com/HPImageArchive.aspx'; +export var BingParams = { format: 'js', idx: '0' , n: '8' , mbl: '1' , mkt: '' } ; + +export function validate_icon(settings, extension_path, icon_image = null) { + BingLog('validate_icon()'); + let icon_name = settings.get_string('icon-name'); + if (icon_name == '' || icon_list.indexOf(icon_name) == -1) { + settings.reset('icon-name'); + icon_name = settings.get_string('icon-name'); + } + // if called from prefs + if (icon_image) { + BingLog('set icon to: ' + extension_path + '/icons/' + icon_name + '.svg'); + let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(extension_path + '/icons/' + icon_name + '.svg', 32, 32); + icon_image.set_from_pixbuf(pixbuf); + } +} + +export function validate_resolution(settings) { + let resolution = settings.get_string('resolution'); + if (resolution == '' || resolutions.indexOf(resolution) == -1) // if not a valid resolution + settings.reset('resolution'); +} + +// FIXME: needs work +export function validate_imagename(settings) { + let filename = settings.get_string('selected-image'); + + if (filename != 'current' || filename != 'random') // FIXME: remove this when we move to new shuffle mode + return; + + if (!inImageList(getImageList(settings), filename)) { + BingLog('invalid image selected'); + //settings.reset('selected-image'); + settings.set_string('selected-image', 'current'); + } +} + +export function get_current_bg(schema) { + let gsettings = new Gio.Settings({ schema: schema }); + let cur = gsettings.get_string('picture-uri'); + return (cur); +} + +export function fetch_change_log(version, label, httpSession) { + const decoder = new TextDecoder(); + // create an http message + let url = gitreleaseurl + "v" + version; + let request = Soup.Message.new('GET', url); + request.request_headers.append('Accept', 'application/json'); + BingLog("Fetching " + url); + // queue the http request + try { + if (Soup.MAJOR_VERSION >= 3) { + httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => { + let data = decoder.decode(httpSession.send_and_read_finish(message).get_data()); + let text = JSON.parse(data).body; + label.set_label(text); + }); + } + else { + httpSession.queue_message(request, (httpSession, message) => { + let data = message.response_body.data; + let text = JSON.parse(data).body; + label.set_label(text); + }); + } + } + catch (error) { + BingLog("Error fetching change log: " + error); + label.set_label(_("Error fetching change log: "+error)); + } +} + +export function set_blur_preset(settings, preset) { + settings.set_int('lockscreen-blur-strength', preset.blur); + settings.set_int('lockscreen-blur-brightness', preset.dim); + BingLog("Set blur preset to " + preset.blur + " brightness to " + preset.dim); +} + +export function imageHasBasename(image_item, i, b) { + //log("imageHasBasename : " + image_item.urlbase + " =? " + this); + if (this && this.search(image_item.urlbase.replace('th?id=OHR.', ''))) + return true; + return false; +} + +export function dateFromLongDate(longdate, add_seconds) { + if (typeof longdate === 'number') + longdate = longdate.toString(); + return GLib.DateTime.new(GLib.TimeZone.new_utc(), + parseInt(longdate.substr(0, 4)), // year + parseInt(longdate.substr(4, 2)), // month + parseInt(longdate.substr(6, 2)), // day + parseInt(longdate.substr(8, 2)), // hour + parseInt(longdate.substr(10, 2)), // mins + 0 ).add_seconds(add_seconds); // seconds +} + +export function dateFromShortDate(shortdate) { + if (typeof shortdate === 'number') + shortdate = shortdate.toString(); + return GLib.DateTime.new(GLib.TimeZone.new_utc(), + parseInt(shortdate.substr(0, 4)), // year + parseInt(shortdate.substr(4, 2)), // month + parseInt(shortdate.substr(6, 2)), // day + 0, 0, 0 ); +} + +export function getImageList(settings, filter = null) { + let image_list = JSON.parse(settings.get_string('bing-json')); + if (!filter) { + return image_list; + } + else { + return image_list.filter((x, i) => { + if (filter.faves && !x.favourite) + return false; + if (filter.min_height && x.height < filter.min_height) + return false; + if (filter.hidden && x.hidden) + return false; + return true; + }); + } +} + +export function setImageList(settings, imageList) { + settings.set_string('bing-json', JSON.stringify(imageList)); + if (settings.get_boolean('always-export-bing-json')) { // save copy of current JSON + exportBingJSON(settings); + } +} + +export function setImageHiddenStatus(settings, hide_image_list, hide_status) { + // get current image list + let image_list = getImageList(settings); + image_list.forEach( (x, i) => { + hide_image_list.forEach(u => { + if (u.includes(x.urlbase)) { + // mark as hidden + x.hidden = hide_status; + } + }); + }); + // export image list back to settings + setImageList(settings, image_list); +} + +export function getImageTitle(image_data) { + return image_data.copyright.replace(/\s*\(.*?\)\s*/g, ''); +} + +export function getImageUrlBase(image_data) { + return image_data.urlbase.replace('/th?id=OHR.', ''); +} + +export function getMaxLongDate(settings) { + let imageList = getImageList(settings); + return Math.max.apply(Math, imageList.map(function(o) { return o.fullstartdate; })); +} + +export function getCurrentImageIndex (imageList) { + if (!imageList) + return -1; + let maxLongDate = Math.max.apply(Math, imageList.map(function(o) { return o.fullstartdate; })); + let index = imageList.map(p => parseInt(p.fullstartdate)).indexOf(maxLongDate); + BingLog('getCurrentImageIndex for ' + maxLongDate + ': ' + index); + return index; +} + +export function setImageFavouriteStatus(settings, imageURL, newState) { + BingLog('set favourite status of '+imageURL+' to '+newState); + let imageList = getImageList(settings); + imageList.forEach(function(x, i) { + //log('testing: '+imageURL+' includes '+x.urlbase); + if (imageURL.includes(x.urlbase)) { + BingLog('setting index '+i+' to '+newState?'true':'false'); + imageList[i].favourite = newState; + } + }); + setImageList(settings, imageList); // save back to settings +} + +export function getCurrentImage(imageList) { + if (!imageList || imageList.length == 0) + return null; + let index = getCurrentImageIndex(imageList); + if (index == -1) + return imageList[0]; // give something sensible + return imageList[index]; +} + +export function inImageList(imageList, urlbase) { + let image = null; + imageList.forEach(function(x, i) { + if (urlbase.replace('/th?id=OHR.', '') == x.urlbase.replace('/th?id=OHR.', '')) + image = x; + }); + return image; +} + +export function inImageListByTitle(imageList, title) { + let image = null; + imageList.forEach(function(x, i) { + BingLog('inImageListbyTitle(): ' + title + ' == ' + getImageTitle(x)); + if (getImageTitle(x) == title) + image = x; + }); + return image; +} + +export function mergeImageLists(settings, imageList) { + let curList = getImageList(settings); + let newList = []; // list of only new images (for future notifications) + imageList.forEach(function(x, i) { + if (!inImageList(curList, x.urlbase)) {// if not in the list, add it + curList.unshift(x); // use unshift to maintain reverse chronological order + newList.unshift(x); + } + }); + setImageList(settings, imageListSortByDate(curList)); // sort then save back to settings + return newList; // return this to caller for notifications +} + +export function imageIndex(imageList, urlbase) { + return imageList.map(p => p.urlbase.replace('/th?id=OHR.', '')).indexOf(urlbase.replace('/th?id=OHR.', '')); +} + +export function isFavourite(image) { + return (image.favourite && image.favourite === true); +} + +export function getImageByIndex(imageList, index) { + if (imageList.length == 0 || index < 0 || index > imageList.length - 1) + return null; + return imageList[index]; +} + +export function cleanupImageList(settings) { + let curList = imageListSortByDate(getImageList(settings)); + let cutOff = GLib.DateTime.new_now_utc().add_days(-8); // 8 days ago + let newList = []; + curList.forEach( function (x, i) { + let filename = imageToFilename(settings, x); + let diff = dateFromLongDate(x.fullstartdate, 0).difference(cutOff); + // image is still downloadable (< 8 days old) or still on disk, so we keep + if (diff > 0 || Gio.file_new_for_path(filename).query_exists(null)) { + newList.push(x); + } + else { + BingLog('Cleaning up: '+filename); + } + }); + setImageList(settings, newList); +} + +export function populateImageListResolutions(settings) { + let curList = imageListSortByDate(getImageList(settings)); + let newList = []; + curList.forEach( function (x, i) { + let filename = imageToFilename(settings, x); + let width, height; + if (!x.width || !x.height) { + [width, height] = getFileDimensions(filename); + x.width = width; + x.height = height; + } + newList.push(x); + }); + setImageList(settings, newList); +} + +export function getFetchableImageList(settings) { + let imageList = getImageList(settings); + let cutOff = GLib.DateTime.new_now_utc().add_days(-8); // 8 days ago + let dlList = []; + imageList.forEach( function (x, i) { + let diff = dateFromLongDate(x.fullstartdate, 0).difference(cutOff); + let filename = imageToFilename(settings, x); + // image is still downloadable (< 8 days old) but not on disk + if (diff > 0 && !Gio.file_new_for_path(filename).query_exists(null)) { + dlList.push(x); + } + }); + return dlList; +} + +export function getWallpaperDir(settings) { + let homeDir = GLib.get_home_dir(); + let BingWallpaperDir = settings.get_string('download-folder').replace('~', homeDir); + let userPicturesDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); + let userDesktopDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); // seems to be a safer default + if (BingWallpaperDir == '') { + BingWallpaperDir = (userPicturesDir?userPicturesDir:userDesktopDir) + '/BingWallpaper/'; + BingLog('Using default download folder: ' + BingWallpaperDir); + setWallpaperDir(settings, BingWallpaperDir); + } + else if (!BingWallpaperDir.endsWith('/')) { + BingWallpaperDir += '/'; + } + + let dir = Gio.file_new_for_path(BingWallpaperDir); + if (!dir.query_exists(null)) { + dir.make_directory_with_parents(null); + } + //FIXME: test if dir is good and writable + return BingWallpaperDir; +} + +export function setWallpaperDir(settings, uri) { + let homeDir = GLib.get_home_dir(); + let relUri = uri.replace(homeDir, '~'); + settings.set_string('download-folder', relUri); +} + +export function imageToFilename(settings, image, resolution = null) { + return getWallpaperDir(settings) + image.startdate + '-' + + image.urlbase.replace(/^.*[\\\/]/, '').replace('th?id=OHR.', '') + '_' + + (resolution ? resolution : getResolution(settings, image)) + '.jpg'; +} + +export function getRandomInt(max) { + return Math.floor(Math.random() * max); +} + +// Utility function +export function dump(object, level = 0) { + let output = ''; + for (let property in object) { + output += ' - '.repeat(level)+property + ': ' + object[property]+'\n '; + if ( typeof object[property] === 'object' ) + output += dump(object[property], level+1); + } + if (level == 0) + BingLog(output); + return(output); +} + +export function friendly_time_diff(time, short = true) { + // short we want to keep ~4-5 characters + let now = GLib.DateTime.new_now_local().to_unix(); + let seconds = time.to_unix() - now; + + if (seconds <= 0) { + return "now"; + } + else if (seconds < 60) { + return "< 1 " + (short ? "m" : _("minutes")); + } + else if (seconds < 3600) { + return Math.round(seconds / 60) + " " + (short ? "m" : _("minutes")); + } + else if (seconds > 86400) { + return Math.round(seconds / 86400) + " " + (short ? "d" : _("days")); + } + else { + return Math.round(seconds / 3600) + " " + (short ? "h" : _("hours")); + } +} + +export function seconds_until(until) { + let now = GLib.DateTime.new_now_local(); + let end, day; + if (until == 'hourly') { + end = GLib.DateTime.new_local( + now.get_year(), + now.get_month(), + now.get_day_of_month(), + now.get_hour()+1, // should roll over to next day if results in >23 + 0, 0); + } + else { + if (until == 'weekly') { + day = now.add_days(7 - now.get_day_of_week()); + } + else { + day = now.add_days(1); + } + end = GLib.DateTime.new_local( + day.get_year(), + day.get_month(), + day.get_day_of_month(), + 0, 0, 0); // midnight + } + BingLog('shuffle timer will be set to '+end.format_iso8601()); + return(Math.floor(end.difference(now)/1000000)); // difference in μs -> s +} + +export function getResolution(settings, image) { + let resolution = settings.get_string('resolution'); + if (resolutions.indexOf(resolution) == -1 || (image ? image.wp == false : true) || // wp == false when background is animated + settings.get_string('resolution') == 'auto' ) { + // resolution invalid, animated background or autoselected + resolution = 'UHD'; + } + return resolution; +} + +export function openImageFolder(settings) { + //const context = global?global.create_app_launch_context(0, -1):null; + Gio.AppInfo.launch_default_for_uri('file://' + getWallpaperDir(settings), null); +} + +export function imageListSortByDate(imageList) { + return imageList.sort(function(a, b) { + var x = parseInt(a.fullstartdate); var y = parseInt(b.fullstartdate); + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }); +} + +export function shortenName(string, limit) { + if (string.length > limit) { + string = string.substr(0, limit - 4) + '...'; + } + return string; +} + +export function moveImagesToNewFolder(settings, oldPath, newPath) { + // possible race condition here, need to think about how to fix it + //let BingWallpaperDir = settings.get_string('download-folder'); + let dir = Gio.file_new_for_path(oldPath); + let dirIter = dir.enumerate_children('', Gio.FileQueryInfoFlags.NONE, null ); + let newDir = Gio.file_new_for_path(newPath); + if (!newDir.query_exists(null)) { + newDir.make_directory_with_parents(null); + } + let file = null; + while (file = dirIter.next_file(null)) { + let filename = file.get_name(); // we only want to move files that we think we own + if (filename.match(/\d{8}\-.+\.jpg/i)) { + BingLog('file: ' + slash(oldPath) + filename + ' -> ' + slash(newPath) + filename); + let cur = Gio.file_new_for_path(slash(oldPath) + filename); + let dest = Gio.file_new_for_path(slash(newPath) + filename); + cur.move(dest, Gio.FileCopyFlags.OVERWRITE, null, function () { BingLog ('...moved'); }); + } + } + // correct filenames for GNOME backgrounds + if (settings.get_boolean('set-background')) + moveBackground(oldPath, newPath, DESKTOP_SCHEMA); +} + +export function dirname(path) { + return path.match(/.*\//); +} + +export function slash(path) { + if (!path.endsWith('/')) + return path += '/'; + return path; +} + +export function moveBackground(oldPath, newPath, schema) { + let gsettings = new Gio.Settings({schema: schema}); + let uri = gsettings.get_string('picture-uri'); + gsettings.set_string('picture-uri', uri.replace(oldPath, newPath)); + try { + let dark_uri = gsettings.get_string('picture-uri-dark'); + gsettings.set_string('picture-uri-dark', dark_uri.replace(oldPath, newPath)); + } + catch (e) { + BingLog('no dark background gsettings key found ('+e+')'); + } + Gio.Settings.sync(); + gsettings.apply(); +} + +export function BingLog(msg) { + if (debug) + print("BingWallpaper extension: " + msg); // disable to keep the noise down in journal +} + +export function deleteImage(to_delete) { + var file = Gio.file_new_for_path(to_delete); + if (file.query_exists(null)) { + try { + file.delete(null); + BingLog("deleted file: " + to_delete); + } + catch (error) { + BingLog("an error occured deleting " + to_delete + " : " + error); + } + } +} + +// add image to persistant list so we can delete it later (in chronological order), delete the oldest image (if user wants this) +export function purgeImages(settings) { + let deletepictures = settings.get_boolean('delete-previous'); + if (deletepictures === false) + return; + let imagelist = imageListSortByDate(getImageList(settings)); + let maxpictures = settings.get_int('previous-days'); + let origlength = imagelist.length; + while (imagelist.length > maxpictures) { + var to_delete = imagelist.shift(); // get the first (oldest item from the list) + if (deletepictures && to_delete != '') { + let imageFilename = imageToFilename(settings, to_delete); + BingLog('deleting '+imageFilename); + deleteImage(imageFilename); + } + } + BingLog('cleaned up image list, count was '+origlength+' now '+imagelist.length); + cleanupImageList(settings); + validate_imagename(settings); // if we deleted our current image, we want to reset it to something valid +} + +export function openInSystemViewer(filename, is_file = true) { + let context; + try { + context = global.create_app_launch_context(0, -1); + } + catch (error) { + context = null; + } + if (is_file) + filename = 'file://'+filename; + Gio.AppInfo.launch_default_for_uri(filename, context); +} + +export function exportBingJSON(settings) { + let json = settings.get_string('bing-json'); + let filepath = getWallpaperDir(settings) + 'bing.json'; + let file = Gio.file_new_for_path(filepath); + let [success, error] = file.replace_contents(json, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); + if (!success) { + BingLog('error saving bing-json from '+filepath+': '+error); + } +} + +export function importBingJSON(settings) { + const decoder = new TextDecoder(); + let filepath = getWallpaperDir(settings) + 'bing.json'; + let file = Gio.file_new_for_path(filepath); + if (file.query_exists(null)) { + let [success, contents, etag_out] = file.load_contents(null); + if (!success) { + BingLog('error loading bing-json '+filepath+' - '+etag_out); + } + else { + BingLog('JSON import success'); + let parsed = JSON.parse(decoder.decode(contents)); // FIXME: triggers GJS warning without the conversion, need to investigate + // need to implement some checks for validity here + mergeImageLists(settings, parsed); + cleanupImageList(settings); // remove the older missing images + } + } + else { + BingLog('JSON import file not found'); + } +} + +export function getFileDimensions(filepath) { + let format, width, height; + try { + [format, width, height] = GdkPixbuf.Pixbuf.get_file_info(filepath); + return [width, height]; + } + catch (e) { + BingLog('unable to getFileDimensions('+filepath+') '+e); + return [null, null]; + } + +} + +export function toFilename(wallpaperDir, startdate, imageURL, resolution) { + return wallpaperDir + startdate + '-' + imageURL.replace(/^.*[\\\/]/, '').replace('th?id=OHR.', '') + '_' + resolution + '.jpg'; +} diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/LICENSE b/gnome/.local/share/extensions/Vitals@CoreCoding.com/LICENSE new file mode 100644 index 0000000..d7f1051 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/LICENSE @@ -0,0 +1,339 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/extension.js b/gnome/.local/share/extensions/Vitals@CoreCoding.com/extension.js new file mode 100644 index 0000000..ee5d735 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/extension.js @@ -0,0 +1,577 @@ +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +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 * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Util from 'resource:///org/gnome/shell/misc/util.js'; + +import * as Sensors from './sensors.js'; + +import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; + +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; +import * as Values from './values.js'; +import * as Config from 'resource:///org/gnome/shell/misc/config.js'; +import * as MenuItem from './menuItem.js'; + +let vitalsMenu; + +var VitalsMenuButton = GObject.registerClass({ + GTypeName: 'VitalsMenuButton', +}, class VitalsMenuButton extends PanelMenu.Button { + _init(extensionObject) { + super._init(Clutter.ActorAlign.FILL); + + this._extensionObject = extensionObject; + this._settings = extensionObject.getSettings(); + + this._sensorIcons = { + 'temperature' : { 'icon': 'temperature-symbolic.svg' }, + 'voltage' : { 'icon': 'voltage-symbolic.svg' }, + 'fan' : { 'icon': 'fan-symbolic.svg' }, + 'memory' : { 'icon': 'memory-symbolic.svg' }, + 'processor' : { 'icon': 'cpu-symbolic.svg' }, + 'system' : { 'icon': 'system-symbolic.svg' }, + 'network' : { 'icon': 'network-symbolic.svg', + 'icon-rx': 'network-download-symbolic.svg', + 'icon-tx': 'network-upload-symbolic.svg' }, + 'storage' : { 'icon': 'storage-symbolic.svg' }, + 'battery' : { 'icon': 'battery-symbolic.svg' } + } + + this._warnings = []; + this._sensorMenuItems = {}; + this._hotLabels = {}; + this._hotIcons = {}; + this._groups = {}; + this._widths = {}; + this._last_query = new Date().getTime(); + + this._sensors = new Sensors.Sensors(this._settings, this._sensorIcons); + this._values = new Values.Values(this._settings, this._sensorIcons); + this._menuLayout = new St.BoxLayout({ + vertical: false, + clip_to_allocation: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + reactive: true, + x_expand: true, + pack_start: false + }); + + this._drawMenu(); + this.add_actor(this._menuLayout); + this._settingChangedSignals = []; + this._refreshTimeoutId = null; + + this._addSettingChangedSignal('update-time', this._updateTimeChanged.bind(this)); + this._addSettingChangedSignal('position-in-panel', this._positionInPanelChanged.bind(this)); + this._addSettingChangedSignal('menu-centered', this._positionInPanelChanged.bind(this)); + + let settings = [ 'use-higher-precision', 'alphabetize', 'hide-zeros', 'fixed-widths', 'hide-icons', 'unit', 'memory-measurement', 'include-public-ip', 'network-speed-format', 'storage-measurement', 'include-static-info' ]; + for (let setting of Object.values(settings)) + this._addSettingChangedSignal(setting, this._redrawMenu.bind(this)); + + // add signals for show- preference based categories + for (let sensor in this._sensorIcons) + this._addSettingChangedSignal('show-' + sensor, this._showHideSensorsChanged.bind(this)); + + this._initializeMenu(); + + // start off with fresh sensors + this._querySensors(); + + // start monitoring sensors + this._initializeTimer(); + } + + _initializeMenu() { + // display sensor categories + for (let sensor in this._sensorIcons) { + // groups associated sensors under accordion menu + if (sensor in this._groups) continue; + + this._groups[sensor] = new PopupMenu.PopupSubMenuMenuItem(_(this._ucFirst(sensor)), true); + this._groups[sensor].icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[sensor]['icon']); + + // hide menu items that user has requested to not include + if (!this._settings.get_boolean('show-' + sensor)) + this._groups[sensor].actor.hide(); + + if (!this._groups[sensor].status) { + this._groups[sensor].status = this._defaultLabel(); + this._groups[sensor].actor.insert_child_at_index(this._groups[sensor].status, 4); + this._groups[sensor].status.text = _('No Data'); + } + + this.menu.addMenuItem(this._groups[sensor]); + } + + // add separator + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + let item = new PopupMenu.PopupBaseMenuItem({ + reactive: false, + style_class: 'vitals-menu-button-container' + }); + + let customButtonBox = new St.BoxLayout({ + style_class: 'vitals-button-box', + vertical: false, + clip_to_allocation: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + reactive: true, + x_expand: true, + pack_start: false + }); + + // custom round refresh button + let refreshButton = this._createRoundButton('view-refresh-symbolic', _('Refresh')); + refreshButton.connect('clicked', (self) => { + // force refresh by clearing history + this._sensors.resetHistory(); + this._values.resetHistory(); + + // make sure timer fires at next full interval + this._updateTimeChanged(); + + // refresh sensors now + this._querySensors(); + }); + customButtonBox.add_actor(refreshButton); + + // custom round monitor button + let monitorButton = this._createRoundButton('org.gnome.SystemMonitor-symbolic', _('System Monitor')); + monitorButton.connect('clicked', (self) => { + this.menu._getTopMenu().close(); + Util.spawn(this._settings.get_string('monitor-cmd').split(" ")); + }); + customButtonBox.add_actor(monitorButton); + + // custom round preferences button + let prefsButton = this._createRoundButton('preferences-system-symbolic', _('Preferences')); + prefsButton.connect('clicked', (self) => { + this.menu._getTopMenu().close(); + this._extensionObject.openPreferences(); + }); + customButtonBox.add_actor(prefsButton); + + // now add the buttons to the top bar + item.actor.add_actor(customButtonBox); + + // add buttons + this.menu.addMenuItem(item); + + // query sensors on menu open + this._menuStateChangeId = this.menu.connect('open-state-changed', (self, isMenuOpen) => { + if (isMenuOpen) { + // make sure timer fires at next full interval + this._updateTimeChanged(); + + // refresh sensors now + this._querySensors(); + } + }); + } + + _createRoundButton(iconName) { + let button = new St.Button({ + style_class: 'message-list-clear-button button vitals-button-action' + }); + + button.child = new St.Icon({ + icon_name: iconName + }); + + return button; + } + + _removeMissingHotSensors(hotSensors) { + for (let i = hotSensors.length - 1; i >= 0; i--) { + let sensor = hotSensors[i]; + + // make sure default icon (if any) stays visible + if (sensor == '_default_icon_') continue; + + // removes sensors that are no longer available + if (!this._sensorMenuItems[sensor]) { + hotSensors.splice(i, 1); + this._removeHotLabel(sensor); + this._removeHotIcon(sensor); + } + } + + return hotSensors; + } + + _saveHotSensors(hotSensors) { + // removes any sensors that may not currently be available + hotSensors = this._removeMissingHotSensors(hotSensors); + + this._settings.set_strv('hot-sensors', hotSensors.filter( + function(item, pos) { + return hotSensors.indexOf(item) == pos; + } + )); + } + + _initializeTimer() { + // used to query sensors and update display + let update_time = this._settings.get_int('update-time'); + this._refreshTimeoutId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + update_time, + (self) => { + // only update menu if we have hot sensors + if (Object.values(this._hotLabels).length > 0) + this._querySensors(); + // keep the timer running + return GLib.SOURCE_CONTINUE; + } + ); + } + + _createHotItem(key, value) { + let icon = this._defaultIcon(key); + this._hotIcons[key] = icon; + this._menuLayout.add_actor(icon) + + // don't add a label when no sensors are in the panel + if (key == '_default_icon_') return; + + let label = new St.Label({ + style_class: 'vitals-panel-label', + text: (value)?value:'\u2026', // ... + y_expand: true, + y_align: Clutter.ActorAlign.START + }); + + // attempt to prevent ellipsizes + label.get_clutter_text().ellipsize = 0; + + // keep track of label for removal later + this._hotLabels[key] = label; + + // prevent "called on the widget" "which is not in the stage" errors by adding before width below + this._menuLayout.add_actor(label); + + // support for fixed widths #55, save label (text) width + this._widths[key] = label.width; + } + + _showHideSensorsChanged(self, sensor) { + this._sensors.resetHistory(); + this._groups[sensor.substr(5)].visible = this._settings.get_boolean(sensor); + } + + _positionInPanelChanged() { + this.container.get_parent().remove_actor(this.container); + let position = this._positionInPanel(); + + // allows easily addressable boxes + let boxes = { + left: Main.panel._leftBox, + center: Main.panel._centerBox, + right: Main.panel._rightBox + }; + + // update position when changed from preferences + boxes[position[0]].insert_child_at_index(this.container, position[1]); + } + + _removeHotLabel(key) { + if (key in this._hotLabels) { + let label = this._hotLabels[key]; + delete this._hotLabels[key]; + // make sure set_label is not called on non existent actor + label.destroy(); + } + } + + _removeHotLabels() { + for (let key in this._hotLabels) + this._removeHotLabel(key); + } + + _removeHotIcon(key) { + if (key in this._hotIcons) { + this._hotIcons[key].destroy(); + delete this._hotIcons[key]; + } + } + + _removeHotIcons() { + for (let key in this._hotIcons) + this._removeHotIcon(key); + } + + _redrawMenu() { + this._removeHotIcons(); + this._removeHotLabels(); + + for (let key in this._sensorMenuItems) { + if (key.includes('-group')) continue; + this._sensorMenuItems[key].destroy(); + delete this._sensorMenuItems[key]; + } + + this._drawMenu(); + this._sensors.resetHistory(); + this._values.resetHistory(); + this._querySensors(); + } + + _drawMenu() { + // grab list of selected menubar icons + let hotSensors = this._settings.get_strv('hot-sensors'); + for (let key of Object.values(hotSensors)) { + // fixes issue #225 which started when _max_ was moved to the end + if (key == '__max_network-download__') key = '__network-rx_max__'; + if (key == '__max_network-upload__') key = '__network-tx_max__'; + + this._createHotItem(key); + } + } + + _destroyTimer() { + // invalidate and reinitialize timer + if (this._refreshTimeoutId != null) { + GLib.Source.remove(this._refreshTimeoutId); + this._refreshTimeoutId = null; + } + } + + _updateTimeChanged() { + this._destroyTimer(); + this._initializeTimer(); + } + + _addSettingChangedSignal(key, callback) { + this._settingChangedSignals.push(this._settings.connect('changed::' + key, callback)); + } + + _updateDisplay(label, value, type, key) { + // update sensor value in menubar + if (this._hotLabels[key]) { + this._hotLabels[key].set_text(value); + + // support for fixed widths #55 + if (this._settings.get_boolean('fixed-widths')) { + // grab text box width and see if new text is wider than old text + let width2 = this._hotLabels[key].get_clutter_text().width; + if (width2 > this._widths[key]) { + this._hotLabels[key].set_width(width2); + this._widths[key] = width2; + } + } + } + + // have we added this sensor before? + let item = this._sensorMenuItems[key]; + if (item) { + // update sensor value in the group + item.value = value; + } else if (type.includes('-group')) { + // update text next to group header + let group = type.split('-')[0]; + if (this._groups[group]) { + this._groups[group].status.text = value; + this._sensorMenuItems[type] = this._groups[group]; + } + } else { + // add item to group for the first time + let sensor = { 'label': label, 'value': value, 'type': type } + this._appendMenuItem(sensor, key); + } + } + + _appendMenuItem(sensor, key) { + let split = sensor.type.split('-'); + let type = split[0]; + let icon = (split.length == 2)?'icon-' + split[1]:'icon'; + let gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[type][icon]); + + let item = new MenuItem.MenuItem(gicon, key, sensor.label, sensor.value, this._hotLabels[key]); + item.connect('toggle', (self) => { + let hotSensors = this._settings.get_strv('hot-sensors'); + + if (self.checked) { + // add selected sensor to panel + hotSensors.push(self.key); + this._createHotItem(self.key, self.value); + } else { + // remove selected sensor from panel + hotSensors.splice(hotSensors.indexOf(self.key), 1); + this._removeHotLabel(self.key); + this._removeHotIcon(self.key); + } + + if (hotSensors.length <= 0) { + // add generic icon to panel when no sensors are selected + hotSensors.push('_default_icon_'); + this._createHotItem('_default_icon_'); + } else { + let defIconPos = hotSensors.indexOf('_default_icon_'); + if (defIconPos >= 0) { + // remove generic icon from panel when sensors are selected + hotSensors.splice(defIconPos, 1); + this._removeHotIcon('_default_icon_'); + } + } + + // this code is called asynchronously - make sure to save it for next round + this._saveHotSensors(hotSensors); + }); + + this._sensorMenuItems[key] = item; + let i = Object.keys(this._sensorMenuItems[key]).length; + + // alphabetize the sensors for these categories + if (this._settings.get_boolean('alphabetize')) { + let menuItems = this._groups[type].menu._getMenuItems(); + for (i = 0; i < menuItems.length; i++) + // use natural sort order for system load, etc + if (menuItems[i].label.localeCompare(item.label, undefined, { numeric: true, sensitivity: 'base' }) > 0) + break; + } + + this._groups[type].menu.addMenuItem(item, i); + } + + _defaultLabel() { + return new St.Label({ + y_expand: true, + y_align: Clutter.ActorAlign.CENTER + }); + } + + _defaultIcon(key) { + let split = key.replaceAll('_', ' ').trim().split(' ')[0].split('-'); + let type = split[0]; + + let icon = new St.Icon({ + style_class: 'system-status-icon vitals-panel-icon-' + type, + reactive: true + }); + + // second condition prevents crash due to issue #225, which started when _max_ was moved to the end + if (type == 'default' || !(type in this._sensorIcons)) { + icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons['system']['icon']); + } else if (!this._settings.get_boolean('hide-icons')) { // support for hide icons #80 + let iconObj = (split.length == 2)?'icon-' + split[1]:'icon'; + icon.gicon = Gio.icon_new_for_string(this._extensionObject.path + '/icons/' + this._sensorIcons[type][iconObj]); + } + + return icon; + } + + _ucFirst(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + _positionInPanel() { + let alignment = ''; + let gravity = 0; + let arrow_pos = 0; + + switch (this._settings.get_int('position-in-panel')) { + case 0: // left + alignment = 'left'; + gravity = -1; + arrow_pos = 1; + break; + case 1: // center + alignment = 'center'; + gravity = -1; + arrow_pos = 0.5; + break; + case 2: // right + alignment = 'right'; + gravity = 0; + arrow_pos = 0; + break; + case 3: // far left + alignment = 'left'; + gravity = 0; + arrow_pos = 1; + break; + case 4: // far right + alignment = 'right'; + gravity = -1; + arrow_pos = 0; + break; + } + + let centered = this._settings.get_boolean('menu-centered') + + if (centered) arrow_pos = 0.5; + + // set arrow position when initializing and moving vitals + this.menu._arrowAlignment = arrow_pos; + + return [alignment, gravity]; + } + + _querySensors() { + // figure out last run time + let now = new Date().getTime(); + let dwell = (now - this._last_query) / 1000; + this._last_query = now; + + this._sensors.query((label, value, type, format) => { + let key = '_' + type.replace('-group', '') + '_' + label.replace(' ', '_').toLowerCase() + '_'; + + // if a sensor is disabled, gray it out + if (key in this._sensorMenuItems) { + this._sensorMenuItems[key].setSensitive((value!='disabled')); + + // don't continue below, last known value is shown + if (value == 'disabled') return; + } + + let items = this._values.returnIfDifferent(dwell, label, value, type, format, key); + for (let item of Object.values(items)) + this._updateDisplay(_(item[0]), item[1], item[2], item[3]); + }, dwell); + + if (this._warnings.length > 0) { + this._notify('Vitals', this._warnings.join("\n"), 'folder-symbolic'); + this._warnings = []; + } + } + + _notify(msg, details, icon) { + let source = new MessageTray.Source('MyApp Information', icon); + Main.messageTray.add(source); + let notification = new MessageTray.Notification(source, msg, details); + notification.setTransient(true); + source.notify(notification); + } + + destroy() { + this._destroyTimer(); + + for (let signal of Object.values(this._settingChangedSignals)) + this._settings.disconnect(signal); + + super.destroy(); + } +}); + +export default class VitalsExtension extends Extension { + enable() { + vitalsMenu = new VitalsMenuButton(this); + let position = vitalsMenu._positionInPanel(); + Main.panel.addToStatusArea('vitalsMenu', vitalsMenu, position[1], position[0]); + } + + disable() { + vitalsMenu.destroy(); + vitalsMenu = null; + } +} diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/helpers/file.js b/gnome/.local/share/extensions/Vitals@CoreCoding.com/helpers/file.js new file mode 100644 index 0000000..a7211b9 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/helpers/file.js @@ -0,0 +1,82 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib' + +// convert Uint8Array into a literal string +function convertUint8ArrayToString(contents) { + const decoder = new TextDecoder('utf-8'); + return decoder.decode(contents).trim(); +} + +export function File(path) { + if (path.indexOf('https://') == -1) + this.file = Gio.File.new_for_path(path); + else + this.file = Gio.File.new_for_uri(path); +} + +File.prototype.read = function(delimiter = '', strip_header = false) { + return new Promise((resolve, reject) => { + try { + this.file.load_contents_async(null, function(file, res) { + try { + // grab contents of file or website + let contents = file.load_contents_finish(res)[1]; + + // convert contents to string + contents = convertUint8ArrayToString(contents); + + // split contents by delimiter if passed in + if (delimiter) contents = contents.split(delimiter); + + // optionally strip header when converting to a list + if (strip_header) contents.shift(); + + // return results + resolve(contents); + } catch (e) { + reject(e.message); + } + }); + } catch (e) { + reject(e.message); + } + }); +}; + +File.prototype.list = function() { + return new Promise((resolve, reject) => { + let max_items = 125, results = []; + + try { + this.file.enumerate_children_async(Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_LOW, null, function(file, res) { + try { + let enumerator = file.enumerate_children_finish(res); + + let callback = function(enumerator, res) { + try { + let files = enumerator.next_files_finish(res); + for (let i = 0; i < files.length; i++) { + results.push(files[i].get_attribute_as_string(Gio.FILE_ATTRIBUTE_STANDARD_NAME)); + } + + if (files.length == 0) { + enumerator.close_async(GLib.PRIORITY_LOW, null, function(){}); + resolve(results); + } else { + enumerator.next_files_async(max_items, GLib.PRIORITY_LOW, null, callback); + } + } catch (e) { + reject(e.message); + } + }; + + enumerator.next_files_async(max_items, GLib.PRIORITY_LOW, null, callback); + } catch (e) { + reject(e.message); + } + }); + } catch (e) { + reject(e.message); + } + }); +}; diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/battery-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/battery-symbolic.svg new file mode 100644 index 0000000..70df9ee --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/battery-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/cpu-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/cpu-symbolic.svg new file mode 100644 index 0000000..19969e4 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/cpu-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/fan-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/fan-symbolic.svg new file mode 100644 index 0000000..a584b74 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/fan-symbolic.svg @@ -0,0 +1,133 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/memory-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/memory-symbolic.svg new file mode 100644 index 0000000..13ca4bc --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/memory-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-download-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-download-symbolic.svg new file mode 100644 index 0000000..5cab840 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-download-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-symbolic.svg new file mode 100644 index 0000000..0483417 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-upload-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-upload-symbolic.svg new file mode 100644 index 0000000..e2c5a08 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/network-upload-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/storage-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/storage-symbolic.svg new file mode 100644 index 0000000..9832999 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/storage-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/system-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/system-symbolic.svg new file mode 100644 index 0000000..909f530 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/system-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/temperature-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/temperature-symbolic.svg new file mode 100644 index 0000000..c58beca --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/temperature-symbolic.svg @@ -0,0 +1,45 @@ + +image/svg+xml \ No newline at end of file diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/voltage-symbolic.svg b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/voltage-symbolic.svg new file mode 100644 index 0000000..b2e7eff --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/icons/voltage-symbolic.svg @@ -0,0 +1,125 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ca/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ca/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..5a18109cbbec78b561fb686e4b07a33e3380dded GIT binary patch literal 3343 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/de/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/de/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..fdc98499322dda3254d461f20f3de137e2eadee5 GIT binary patch literal 8159 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 Nt7HHlYrP~w{{!LoK0N>c literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/fi_FI/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/fi_FI/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..38454e3df1fc067585e343ab4af5fc7527d3a6b3 GIT binary patch literal 4004 zcmb7`ON<;x8Gs820k6X&4zJiiDh3A!duAVg;EW$(9eeHdYe(L-iG@g1J5#$;Gd*2R zSM}Hv2_=yNNKr&kB*+1Yhq711i9Nt^xS&Y6AwZyT$bmy71cC#KIPm>FJ-cxdAJUrY zuO5H@_5XkURW+~gy6Z=Z=M?4hlzTp?)H3|vhj{Uv|FBa0)FQ7Pa0%WAZ&d9*{0Qxv z@Gkf?7YE6#2)Y$bYNqZ^KW~?pEVZL(%^n6nkEPV*kr4n_a3;V$@U#n++e z`B&BcFBCg>5_GZuUMTso8;bvRDDxew#up*3sjE=tc>;=^0gC)HQ2c!!ioTcN1MnwR z|8FafAXBSX;b-8Vq1gLRD0=TEX+_^2D0=omnYRvQz6SgPJO@S2LXq=O_RlRSdHWJ% zIm$=cGgbWrlzF~P*;Dl$gc1vR#1=Ux_EY94^3*7GimaDBk5I%Wc_e0yvPP_+*d~wI zB+pkU4^qT-d8R23m9_EuNQ~udlHC3(TD4;TaJwP>(Q#N=2v|Y)qIlpmWj4a zE$f(78{Jb@2b%_;2Q@!1E-|Z#rDk)pZq;cQgF5Yfi0bq(+Mph@X|%a|%;#3`YpVs5 zss(FeK{L1CE^LTF`&_RU>)}z|Osol34_t6jpD?J27UVF+IKRVy9TGw|sxP zg}Rn+yL5f}x{D?W(=G&*?LBqQ#q-7LiD`4HuCI7~)8x5Ht~6x z?fSbX+16Z6)b=Uf*RxKx_tcrPE~>R#(?ehAHD9D{9Xl@QBG|PesjCH__f1p_xwZ1b z!?ZP2=WI%R5_R6St$xbpUJp!yy=pOSB?Wz1v6{G+UTkX9Zi}7jl3k1Hk~eKVd!%1g zD$>4e`#vA4%RY4xf7S@=q~5=MD7EpX&wE?LS?*it_xV;&K!dE^L_rvaXqlqYO+CYS zS+cFZfls1*a-~wfl-X1-xCD=yCU#V_GYqbU!%fdpI-hEn>dd5;9@f<6L`7uBw&)et zx042jH7CJPS1tZCw;k7_XRWEO5upwYHXo3hSKWFis%0`FZ3j(`-n5ugWNkhS^pBHu z`KHO#^4PaAbmgL2A<+z<&bOi)*sbPDIoE#_+I5@DXAM2oT&i8QlK4KYo#6|*E}0N(E2M8o z3`^9|XA@^IGEcoe8+tZzDQ=#CCe-bu0XV0e0Jj8>sH6)!N!xUEIT$ihtD)=lsb*5Jn`*MxLPMun*$gM=j_YytN8`x;SFW3#99V&Rl!65j8IJ(|*e??!Dn?vpq}_$3;8ZsH@8bTbgyu zj7beqA4mU2V&w!-P3IC$DjkO%BFd_=TpWjE&m^L{TO_^muvl!?%4k}Y1>5hNa*c7O z?_v?ko95zntFN{ja_1C;!SQ7>@H$pY93{V`qhF3rTKpX#wI(eI7wMC333H4mHcbok zC1xTu$wQV$7NXM2+Vr#0<~syaj$^V}q{^8j+1vcmT-mHHYzI$qG8?10x!F(C!S)LD-PWnD zySzu3O7q9tZprm*8Run@$8P@LlrnM^rFuaAvDNrse!MI3+jW>mU8weD8IRjrDSsaZ zoYHvEi_XctVf6B7gQ=X%y;|7`2 n6xjS>9eQ1>Bk;> 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 KoMG6*dh;K8?)H!X literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/it/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/it/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..f37507d4bb89eb8c2d3d3fd4cac2ef8f6ad3251f GIT binary patch literal 3030 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 K%Q9umRsR7-_EiZ0 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/oc/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/oc/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..ef097f71c3e1d4538c744db7f6052fd45b96bee5 GIT binary patch literal 3817 zcmb7_TWlRi8Gxq{E_DlS!!0e)4uQ1L^!S!0O-~Y6jvXgPNqz7omSM{@p#c(>4zs z?d&)A|DS*U`R9-O_dTmfQ{)ekZ-0wYOYqgV@*|yln^Jsgp5Lu-0lo)*rl}|J9n{z1 zKKLZ$Q%^ViDttTjZ$KIM9F(Ph0AGSvoBk^dlKEeTBIg+>gx`mE!)s8+{T99x zz5r$1iw*w-`PAR|-3I>&-wj`Zcfea2d>6bMO8qdD{u5C4^~t9FbC6G^{6^p!6uDo7 zBKJ#B*7-UVIX{HA!=FIW|CbGa-S7n{>-+)oslV`(b^ZY{f%*^p0DJ}Vsr__{ynCSR z=RPQU-rw*jd>8duDDxMgj9-N^{^nVWC$=P3pqW@3n zly!auKL&pTW&Gcp_WweW_bOzHx`jn#{7A#QpvXA@WuAtAg@=dz7)$g%0BKV9LK*)c z6hEAV_re7z`lJmnLy`AX!>={`78Lt^555on42s|U4!$401V0G>-Sppq@eWWw1ZDgb z6u(@CVy{&w_D-Pa`31NiJ_Y&Iv;4$gu0ql8=TP?feAE7WDE{^5ru|<~^!^_dJMClf zkHFiY%x^(ye+0_7DJc4%g<_x8hGEm6Lm9sTMXxJO{hLtq|1Okut~LAxlzl!A#cyAP zw?Ii!AFoPEorE%9&Rpyz=^?V%K$5H>=ksy$Bw136JW7`HlJqE9be1GGYF8ztj>8W( zWzkL217xuS$5N#+{`WU!*_YTrVw3pVgXD+FV&{9v5)X#yJ_-k$ik!FjlGx`E`7l|N zA0Zzlf0Qg|bA)_?{0Z_fiBE7u)!9p%oJ|u7)hWtc5j8=UB>r}+D)F1{W~EotahwIl z+mKQ|FkS1TO{<+vT~;_Xy=J1=bYn}&7N&32>Bu{EI?J3_r#HNHYR0DC7HTFdtUeT~ zIg_e6YkWzyuuqiMd8e~NcgwzeOm|{yoYiZQi@ZK*^q{c4QwIm$=dL|A){p$4?2ZOm zG8V90kcCm&AG;WN6T7jq${a${F&{d!h>ey2MxvViroVyDHif}GuO0k`42MMSeU zC3aXrvChyKYSFTPWZSyrGtQb~4+{;Tv&9;&xft~aUM=AfY3MXQx^7XY%tIzR z+G}TBS~s~`s`pkyH(ON8IGSPVbl{^kyHj1R#MS>546RmjFPg46+4~BfIzX~5G@cdU zvso&Qp}N>u=hH>xadT0>F4p?9>q|E9t@*HZ(H1hTtw%Zwtwk%2pQWu?X3%urINw^v zeO+ugqPG4N&g3yWq@@rL{9G^JenwV@& zKB_0i+mpu*k57z`kF2(8x36^XWH#hdA3>4TR$XbG=CbFwOIweQ?zS#jQv?HXqtFWo05PUXMLi_LJ`{36xzhSD&z(R*%i1l|&iB>;1CXST093BVV{g<=zO&1O*}4UH!J^s+QS zYZDVpz0M>YzkKD|V;Y}vC0E^VzCKdE>2N$_a*@?H5|N|O)G;|qC?c{7$pP};d{8c` zn9OzbMb;Hv%@gJJbzQR2oq&8k>SBc|B5(hg%^Ed~$%GLJ96U&|apG^hY})9}?X4oK z? zdlGxv4Q`ga!RW#g;r347jpKWo(b6V~>6^`ncZ(pkr!Uhka(;U&xj_#@BpAl`8#+)> z!k8?wi0s*{TSTI_)7+Io#IErzlMs%=2TTxbZ#gwb_zJ3!FV2MbO32^d8pizE?oKTu z(1|Db!UVo@?LEPNk9Fn24LOt8_VE!n=4>B5ZfH}4>=L)##Nq4rfa^0$I3BPW+vi-F zU)_M}bdgt&76nEB;bmH4OG#HP4J|Z${B}&n tslALCc`n3?o#7*)6BFq^mmoB11s_FuU%weLY`8(ZvcS-<+4fGW`ajgu!dCzQ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pl/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pl/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..b42daa4e2618f669eef729de7b2ab90336cd30d2 GIT binary patch literal 3501 zcmb7_U2I%O700L0P*|WJp#%tDQov)PY8sYzMwvVc&RK!+^G84aE=Y3U2kAWO(0NBd z+J8St@;Uek@DxaTKjFCm;>VUy(Y|#MS7k4Np8-PzXPEf`-2~U3w#*;cl`W6K>XPIsFvVMQi9}PgOa50H^EPXZIJZ-4l26O z%OJ`BAxP)l0ZZVILHyXyeE%2VB;@|ukN+KuX#GDy(*GU^qw|k~$H3zt`LF87p9N{% zDoB0_Jsn8)x&=N8z5;$8e8bQG9^~l%1tdHE2mBIv440yF&Vl5|t03v~Ef7ms10=aA zNdEh_AAbcTJ+}S)PeJ_H4l26uuYCWvAnpGXNc#NE_y6VRkHATEkE0;jbqqWTp7(qf zBzZ4*wm@2+f@G&Hko0;PB)`4}()!myavw*b^XdB{3Y}4=j{nHE^wHkWqMSt`xf3XK zKZs!`QOH&#m%c9#Q0Q?S?XUU%*Fn;K_>mpY`OY}_6_hh5WM_KIlrJc@PNRGX1twt? zlu4A)uj)Hr^(6n&_ZZ446pB0YDcON+OyAT1MNR&F-1o_+V<;5!C;S-YuHi>9`v`UB z>n3w-K8{TwoD9)QTB0tU>Pc>;wkEY~ep9Ge)MJU3Ohr?&OUhYxDR;vXxzJ!w34y8%SD?+bJgIv$#}zLI^?dUEYGZLWO0QpnzSPvTTG>- z3OCe2&YqGQmWkQZDwMn@Q^Pk!41L)Z9mE;NlB^!9fM2O`5r(8EyDA%wT{R-))pH%+ z@I-$}c1)VHC8L$YeHt+9u-!R4rKRhdG}@m|QWHSFN%u!AB#_sJ0&H%b#1URL+6j~= zm+g;jn%L2nYhHLwxE8Cma;pN|vS#2vWi(fsCqhe%z>zYADRAZ5Kmop{I&#>7HnpJz z*othx0xi(WbQ3|hqM9woR^f3ST8`-JN~w94gm8_;_#g~dyCPw$h3?$z&ejfe=NU}J z)(}zxLgoXfHsyYI%^--_pkAkS>xm=v)-By~9WmL04l?D4p#afoG!2IAy0-)F)RuuO zUkS_CWlHnYyi{8*uSkkzqsy0}Du%8K>&k10NE-`SeVRXGAbUl%1fOZ6S$(`({YJBc z_e9Y*hxNLdL51>~6rE!Kpg+x%xU_U->B)oRs+FnIg27|cI3Z7Pnv*x&WD*OdFYurx zQY+o;`r2ao{J}cX1db^`sRyrVnxC&LSE|JsqP0x1Y^S+SimshKa{e&fwsbb6Ni3_FY%7aHyn4xB5 zh$nxhGF7RT*fLK|&*GW%q*fVXq?d<1C1bolw%*^myVKw0B5qI~%7dO(l3g~|?QJKR z@4uSLP%ASoCK#@gv$|-bz9xA8PHMs^V=G}#_HR)#+JLR{!Vw7B(9{KAP~57$?TdWF zCZG58(zlFE8_Y$x7xAwNf{32ftyGUgHUvlk5v}NSb;U84yf9227 z#QB(#l~UKMhm)LuW zEjRD((8PL_=50#D4 l3y3^-b4ak9HhOT&XM-;=8^x{^E{Sa*=Ec2K<(Y`?;-?b zkpd47bPg{-O00cFq9SM7Lj(K$f1pc{5m>)kYe|?M|NqE;6~7mtrm*#{;TNpcO${sqUurX z3s@43wjh@mxt89GPquCHi?CcoF7N8X{yyRE=UXy8jJ_Q9@4&_VUG{t`@$kB)i@XhA zB&jHzF`O6Vjqxu2`y>4IiJ&!sBeJ$ukz%!2MZBALl+XwNOvB6W2Miw4@c$%d^Mz)4 z(Bj>_+b$x{Z1@I8gx?;#?*4a&t$itjiy0pik literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..ebf4c1123afc32a7faca20e4fe9504cb69f9fcf1 GIT binary patch literal 3935 zcmb7`ON<;x8Gs82fh++MCp=;vDH{?JoSB_jKVpx)4r{O1-pG&9K8%sTQf+tbPAxNC zLs#`U>jMTU3Ic_P5FrPoAmoDq$pQ(%!3Pcu7cP-FkpnLY2@Xi{1%wnp@clhKJ8~Q& z(VFV7US0M7fBp6U^ZOlly`XrGQa(tz_svQz!+*Vn7tg7;D#cGN^126}gAc$@RqY;p z8|_Q*E_em4H=xXW5z5lvg};SYtMM-|NxuIQlzpCuGXLA~KKK%R2>uAl zykEh$!^}!(-V!evzX)YN3q`I?C~|B;(fruG$x&*kiL|gtG4E zp@z>vk^dSLd;GW>{}~kde-8PnUswDilzBIytn+s$^ZpIxUfqXNzaKsbW#1ZI~p!my2DPk{qo}kE{@`&vuPCQ1Lp~zFC)F~g92G1di*hC(& zS*xsJBlw}JE;`HeG0H9q$5}qpydSOVqL;`lu}RM6XkDUVaekJwJmPi!QQ*suzPYL+@zL^Ub$h`${uYrN*x zd=b@Lx9bxVZAz`*F>5xu%T@=Q1)m2sH!!Yi*1DFO&CP~Y$6O5RnD-&7V_VS%b=+pr z=IXf5t$rj`3no(w*2IElZa-Jp5QFx)UMn`jQ@Y)?CRjai!9{(<=uU3eXLomE?1$F$ z^oEO_Vy&L|-gLrpiBDa&F@4@e(+$%u1Qy$U^rVZYinSxt=4f4?_4<;@bCbobC$xR0 zpW6^@D%-hUzw2yTzkQR*%H>9>&(OZ!@AP*coiFRMw07%y%NKgx7g?%f#|2#kyIyqb zYQg6{6V*a)t-R1MOSaTWn_-`>I^|NUFWcPff$1WzTFjDeL0`XE>$*fQwzWx9kyD+v z>rtKdCe_XTy{b}?_8Hsr`Ib84GZ)ck9kY(>y<3Me8!!3%v)jY{+$XH>^X;C%3UYSi z1;SQ{mMyBasrM0Hj%>Sc;JZ;iIa@7$u5UBF;JRqkHnF4Hovq*!6mENt()mogO!rM@ z>ETRmj#XstNEW@~dUo7Eu=dy(YRRHMd)rYhdd`~aIu`1{VDka4x#TuFQ7z*USsFAx zddVVA(NFm>&_8n4iQB&^4T1OFvz7oyok$m6m;R+34e@ zXYviNk8F-cLh=btKKiW3l@-@M$|BiDfhM&TYkDo+_IWHea7wF>=*ehH_v6%=#WN=+ z!Zz!TspFn|ogu_lA1R}KCQkRe#$|_f(lL3kadvfOp>}XGPlV;*YbUbN#ck<>Yc8Vk z%IzvQ57wG9wV6Y@*=Wri*wbjD@`c*y5|!$njN7{FeGSC9P-}aex>!3#0O{j`EnTlq zwYvqkt|oUfv~<=ln_+h5upU)sAItQvS>1em^8Kb@>jN(Dnc)voXGT$ePGYD?KD;)(>Z@=+ z=XHWxf?R+jv#ZT>Pa-%k8Nh1X z_y0)(uZ#XR>l8hsH;iNeXSP#-kqspS46i37kGft!1*41J?YsStY*1zfH}GXX;LZ>M zgTZP!Cnez^_2XpV)oC{%c$Vp4_(OCz>SUP?&LstLG#57`p#_~4U0>w{%;dGrQq~$? z^SVF0Aqk?q;EQc_B9K^ePU|?A+(?FS*|)rcVlUdD-V+30WC*X!UUxx8MQNshUFBn#qROCrF6!+RYb zqWLG1M{Gry!?Cnr>c5&#?sfnG literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt_BR/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/pt_BR/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..ddcca6d47d22958791c588c99c9626db544e5a19 GIT binary patch literal 7407 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 WH(aN=7K;u)4zM&SL1*AY!~X%hfcP)~ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ru/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/ru/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..1f58435ca13ab609516a096a9b92757bb1a9b727 GIT binary patch literal 10711 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/sk/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/sk/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..73a62eae8f3dbdcc96950026767b827b4a5a8a4d GIT binary patch literal 8559 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 TGni3jxETbMfw@Gg3?TmvS6+-W literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/tr/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/tr/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..eee9eddecf6a2b8a7643ca5f9799b8c7bc807079 GIT binary patch literal 8416 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 PQGvu4Rqek@%T513?2sGM literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/uk/LC_MESSAGES/vitals.mo b/gnome/.local/share/extensions/Vitals@CoreCoding.com/locale/uk/LC_MESSAGES/vitals.mo new file mode 100644 index 0000000000000000000000000000000000000000..c24b01bae2958aa89d815b602c200fbd2c0f01bf GIT binary patch literal 10967 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 l#_5~PO { + this._settings.set_boolean(sensor, val); + }); + } + + // process individual drop down sensor preferences + sensors = [ 'position-in-panel', 'unit', 'network-speed-format', 'memory-measurement', 'storage-measurement', 'battery-slot' ]; + for (let key in sensors) { + let sensor = sensors[key]; + + widget = this.builder.get_object(sensor); + widget.set_active(this._settings.get_int(sensor)); + widget.connect('changed', (widget) => { + this._settings.set_int(sensor, widget.get_active()); + }); + } + + this._settings.bind('update-time', this.builder.get_object('update-time'), 'value', Gio.SettingsBindFlags.DEFAULT); + + // process individual text entry sensor preferences + sensors = [ 'storage-path', 'monitor-cmd' ]; + for (let key in sensors) { + let sensor = sensors[key]; + + widget = this.builder.get_object(sensor); + widget.set_text(this._settings.get_string(sensor)); + + widget.connect('changed', (widget) => { + let text = widget.get_text(); + if (!text) text = widget.get_placeholder_text(); + this._settings.set_string(sensor, text); + }); + } + + // makes individual sensor preference boxes appear + sensors = [ 'temperature', 'network', 'storage', 'memory', 'battery', 'system', 'processor' ]; + for (let key in sensors) { + let sensor = sensors[key]; + + // create dialog for intelligent autohide advanced settings + this.builder.get_object(sensor + '-prefs').connect('clicked', () => { + let transientObj = this.widget.get_root(); + let title = sensor.charAt(0).toUpperCase() + sensor.slice(1); + let dialog = new Gtk.Dialog({ title: _(title) + ' ' + _('Preferences'), + transient_for: transientObj, + use_header_bar: false, + modal: true }); + + let box = this.builder.get_object(sensor + '_prefs'); + dialog.get_content_area().append(box); + dialog.connect('response', (dialog, id) => { + // remove the settings box so it doesn't get destroyed; + dialog.get_content_area().remove(box); + dialog.destroy(); + return; + }); + + dialog.show(); + }); + } + } +}); + + +export default class VitalsPrefs extends ExtensionPreferences { + fillPreferencesWindow(window) { + window._settings = this.getSettings(); + + let settings = new Settings(this); + let widget = settings.widget; + + const page = new Adw.PreferencesPage(); + const group = new Adw.PreferencesGroup({}); + group.add(widget); + page.add(group); + window.add(page); + window.set_default_size(widget.width, widget.height); + widget.show(); + } +} diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/prefs.ui b/gnome/.local/share/extensions/Vitals@CoreCoding.com/prefs.ui new file mode 100644 index 0000000..76d1473 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/prefs.ui @@ -0,0 +1,1211 @@ + + + + + 1 + 60 + 1 + 10 + + + 0 + 12 + 12 + 6 + 6 + vertical + + + 0 + + + 0 + none + + + 100 + 1 + 0 + + + 0 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Include public IP address + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + 6 + 6 + + + 1 + 0 + start + Format + + + + + 0 + 0 + + Bytes + Bits + + + + + + + + + + + + + + 0 + 0 + 6 + 6 + 6 + 6 + vertical + 2 + + + 0 + 1 + 1 + 1 + + + 0 + 12 + 9 + 12 + 12 + vertical + 12 + + + 0 + start + baseline + General + + + + + + + + 0 + + + 0 + none + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Seconds between updates + + + + + 5 + 5 + 0 + 2 + update-time-params + 1 + 1 + 1 + if-valid + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Position in panel + + + + + 0 + 5 + 0 + + Left + Center + Right + Far Left + Far Right + + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Use higher precision + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Alphabetize sensors + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Hide zero values + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Use fixed widths + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Hide icons in top bar + + + + + end + + + + + + + + + 100 + 0 + + + 0 + 6 + 6 + + + 1 + 0 + start + 5 + Menu always centered + + + + + end + + + + + + + + + + + + + + + + 0 + start + baseline + About + + + + + + + + 0 + + + 0 + start + baseline + 5 + Feature requests or bugs? Please visit <a href="https://github.com/corecoding/Vitals/issues">GitHub</a>. No warranty, expressed or implied. <a href="https://corecoding.com/donate.php">Donate</a> if you found this useful. + 1 + 1 + 0 + + + + + + + + + + + + 0 + 6 + 9 + 12 + 12 + vertical + 12 + + + 0 + start + baseline + Sensors + + + + + + + + 0 + + + 0 + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor temperature + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor voltage + + + + + end + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor fan + + + + + end + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor memory + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor processor + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor system + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor network + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor storage + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + + + 1 + 0 + start + 5 + Monitor battery + + + + + 0 + 6 + + + 1 + center + center + + + 0 + emblem-system-symbolic + + + + + + + + end + center + + + + + + + + + + + + + + + + + + + + + + 1 + 12 + 12 + 6 + 6 + vertical + + + 1 + + + 1 + none + + + 100 + 1 + 0 + + + 1 + 6 + 6 + 6 + 6 + 12 + + + 1 + 1 + start + Path + + + + + / + 1 + 1 + 24 + + + + + + + + + 100 + 0 + + + 0 + center + 6 + 6 + 6 + 6 + + + 1 + 0 + start + Measurement + + + + + 0 + 0 + + Binary + Decimal + + + + + + + + + + + + + + 0 + 12 + 12 + 6 + 6 + vertical + + + 0 + + + 0 + none + + + 100 + 1 + 0 + + + 0 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Unit + + + + + 0 + 0 + end + + °C + °F + + + + + + + + + + + + + + 0 + 12 + 12 + 6 + 6 + vertical + + + 0 + + + 0 + none + + + 100 + 1 + 0 + + + 0 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Measurement + + + + + 0 + 0 + + Binary + Decimal + + + + + + + + + + + + + + 0 + 12 + 12 + 6 + 6 + vertical + + + 0 + + + 0 + none + + + 100 + 1 + 0 + + + 0 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Display Battery + + + + + 0 + 0 + + BAT0 + BAT1 + BAT2 + CMB0 + macsmc-battery + + + + + + + + + + + + + + 1 + 12 + 12 + 6 + 6 + vertical + + + 1 + + + 1 + none + + + 100 + 1 + 0 + + + 1 + 6 + 6 + 6 + 6 + 12 + + + 1 + 1 + start + Monitor command + + + + + gnome-system-monitor + 1 + 1 + 24 + + + + + + + + + + + + + 0 + 12 + 12 + 6 + 6 + vertical + + + 0 + + + 0 + none + + + 100 + 1 + 0 + + + 0 + 6 + 6 + 6 + 6 + 12 + + + 1 + 0 + start + Include static info + + + + + end + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/gschemas.compiled b/gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..7f4d0c0245fb00f704dd9dffffc261d44f72d7a1 GIT binary patch literal 1780 zcmZ8hPiP%Q9G+@pYtxuCtu<8xDWbfm?0XHB*djtff*w2sG@@RXdGAfKqq{rn&TR4$ ztMpKzik`d_G{l<)seevN1<57YV-8ZhiFheOQw62uRKIU#pJ`qA_`dh;yqVwpX7+pG zRat78J5fIq;Mam~+*5mdz}H&8&5-%-xOh>V1%Gp|5JL|NaU0K#`(TKd#Gl~50aGrv z7ITxSmQ7Tewp8D#+-j5C){=HIwHz))V=}PdTEt#p2-sI+KlpxN7#OKB3MR&C90HHu zfy2-b0o=;~$Dkhu9swQ&wqJhi75rd~{r)E7Dz#jvR zeepJZ>izI9g4clGb9I?M^%3~rgExqG-y7z6P!GYs3f=~;U-|Acj;H4QZ1NEBZ1;%< zedia{@dM<+3fWdvD=6(8{?{S}Qu1{a%Jk%UN0iFVWUSHetb=Lj~@C#-~JsSjZM8SpId)$_O8^r?9+bKrU4)`veHrB8hrege*bk)6>deQNsWz!!ko>ZfJ; z)V_Z38t{?%;0^lJoc|K|GVspK&T0D8?DGbA6X;EMR_If6{1*5Z;MZ#Q2l~{k|2p^v zU=y<(b)<8uT)}HRjZQN$F0%N%OljjxWu{z-D=QaOEMmK2oyy|W$X-01!H9F^mQDFq zRQBUc_TyNLPMs7DX`6oPFkXl~oVUvLf5G6F(#_1kGiB7xdVY2PjYr{MQT-A%*=DFC z%9ONKsWMeZ3^PPDbXcq|g_d8>SFj-S!S6w-wos}UEtoQs|F5T2NS9!I8-dgI#k76k zw08ri9RjC~=7lYyel&-x*6S9=YKLr!bRHElS82#1)>&B>;!4D8SEzxromi`M^6A9_A-A4_5}?Bkxey6LXW9$b4i@BHnx^9t79dDzd2w zZRRrG-;U>DLBHcxr`~AwaNMuJjy{h~^b>2-YjId5R-9=XyOz2BJ*&UbS yuKTcLQdghMZok4sx@+E|>ZF{v7~IpMC!LC%&J_E>bMqChs?rP^%5&j6JpTd@+h;TY literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/org.gnome.shell.extensions.vitals.gschema.xml b/gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/org.gnome.shell.extensions.vitals.gschema.xml new file mode 100644 index 0000000..71e4935 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/schemas/org.gnome.shell.extensions.vitals.gschema.xml @@ -0,0 +1,140 @@ + + + + + ['_memory_usage_', '_system_load_1m_', '__network-rx_max__'] + Sensors to show in panel + List of sensors to be shown in the panel + + + 5 + Seconds between updates + Delay between sensor polling + + + 2 + Position in panel + Position in Panel ('left', 'center', 'right') + + + false + Use higher precision + Show one extra digit after decimal + + + true + Alphabetize sensors + Display sensors in alphabetical order + + + false + Hide zero values + Hide data from sensors that are invalid + + + true + Monitor temperature + Display temperature of various components + + + 0 + Temperature unit + The unit ('centigrade' or 'fahrenheit') the extension should display the temperature in + + + true + Monitor voltage + Display voltage of various components + + + true + Monitor fan + Display fan rotation per minute + + + true + Monitor memory + Display memory information + + + true + Monitor processor + Display processor information + + + true + Monitor system + Display system information + + + true + Monitor storage + Display storage information + + + true + Monitor network + Display network information + + + true + Include public IP address + Display public IP address of internet connection + + + 0 + Network speed format + Should speed display in bits or bytes? + + + "/" + Storage path + Storage path for monitoring + + + false + Monitor battery + Monitor battery health + + + 1 + Memory measurement + Can use gigabyte or gibibyte for memory + + + 1 + Storage measurement + Can use gigabyte or gibibyte for storage + + + 0 + Battery slot to monitor + Which numerical battery slot should vitals monitor + + + true + Use fixed widths in top bar + Keep sensors in top bar from jumping around + + + false + Hide icons in top bar + Keep top bar clean by only showing sensor values + + + false + Make the menu centered + Center the menu to the icon regardless of the position in the panel + + + "gnome-system-monitor" + System Monitor command + The command run when system monitor button is clicked + + + false + Include processor static information + Display processor static information that doesn't change + + + diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/sensors.js b/gnome/.local/share/extensions/Vitals@CoreCoding.com/sensors.js new file mode 100644 index 0000000..39d175a --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/sensors.js @@ -0,0 +1,671 @@ +/* + Copyright (c) 2018, Chris Monahan + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import GObject from 'gi://GObject'; +import * as FileModule from './helpers/file.js'; +import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; +import NM from 'gi://NM'; + +let GTop, hasGTop = true; +try { + ({default: GTop} = await import('gi://GTop')); +} catch (err) { + log(err); + hasGTop = false; +}; + +export const Sensors = GObject.registerClass({ + GTypeName: 'Sensors', +}, class Sensors extends GObject.Object { + _init(settings, sensorIcons) { + this._settings = settings; + this._sensorIcons = sensorIcons; + + this.resetHistory(); + + this._last_processor = { 'core': {}, 'speed': [] }; + + if (hasGTop) { + this.storage = new GTop.glibtop_fsusage(); + this._storageDevice = ''; + this._findStorageDevice(); + + this._lastRead = 0; + this._lastWrite = 0; + } + } + + _refreshIPAddress(callback) { + // check IP address + new FileModule.File('https://corecoding.com/vitals.php').read().then(contents => { + let obj = JSON.parse(contents); + this._returnValue(callback, 'Public IP', obj['IPv4'], 'network', 'string'); + }).catch(err => { }); + } + + _findStorageDevice() { + new FileModule.File('/proc/mounts').read("\n").then(lines => { + for (let line of lines) { + let loadArray = line.trim().split(/\s+/); + if (loadArray[1] == this._settings.get_string('storage-path')) { + this._storageDevice = loadArray[0]; + break; + } + } + }).catch(err => { }); + } + + query(callback, dwell) { + if (!this._hardware_detected) { + // we could set _hardware_detected in discoverHardwareMonitors, but by + // doing it here, we guarantee avoidance of race conditions + this._hardware_detected = true; + this._discoverHardwareMonitors(callback); + } + + for (let sensor in this._sensorIcons) { + if (this._settings.get_boolean('show-' + sensor)) { + if (sensor == 'temperature' || sensor == 'voltage' || sensor == 'fan') { + // for temp, volt, fan, we have a shared handler + this._queryTempVoltFan(callback, sensor); + } else { + // directly call queryFunction below + let method = '_query' + sensor[0].toUpperCase() + sensor.slice(1); + this[method](callback, dwell); + } + } + } + } + + _queryTempVoltFan(callback, type) { + for (let label in this._tempVoltFanSensors[type]) { + let sensor = this._tempVoltFanSensors[type][label]; + + new FileModule.File(sensor['path']).read().then(value => { + this._returnValue(callback, label, value, type, sensor['format']); + }).catch(err => { + this._returnValue(callback, label, 'disabled', type, sensor['format']); + }); + } + } + + _queryMemory(callback) { + // check memory info + new FileModule.File('/proc/meminfo').read().then(lines => { + let values = '', total = 0, avail = 0, swapTotal = 0, swapFree = 0, cached = 0, memFree = 0; + + if (values = lines.match(/MemTotal:(\s+)(\d+) kB/)) total = values[2]; + if (values = lines.match(/MemAvailable:(\s+)(\d+) kB/)) avail = values[2]; + if (values = lines.match(/SwapTotal:(\s+)(\d+) kB/)) swapTotal = values[2]; + if (values = lines.match(/SwapFree:(\s+)(\d+) kB/)) swapFree = values[2]; + if (values = lines.match(/Cached:(\s+)(\d+) kB/)) cached = values[2]; + if (values = lines.match(/MemFree:(\s+)(\d+) kB/)) memFree = values[2]; + + let used = total - avail + let utilized = used / total; + + this._returnValue(callback, 'Usage', utilized, 'memory', 'percent'); + this._returnValue(callback, 'memory', utilized, 'memory-group', 'percent'); + this._returnValue(callback, 'Physical', total, 'memory', 'memory'); + this._returnValue(callback, 'Available', avail, 'memory', 'memory'); + this._returnValue(callback, 'Allocated', used, 'memory', 'memory'); + this._returnValue(callback, 'Cached', cached, 'memory', 'memory'); + this._returnValue(callback, 'Free', memFree, 'memory', 'memory'); + this._returnValue(callback, 'Swap', swapTotal - swapFree, 'memory', 'memory'); + }).catch(err => { }); + } + + _queryProcessor(callback, dwell) { + let columns = ['user', 'nice', 'system', 'idle', 'iowait', 'irq', 'softirq', 'steal', 'guest', 'guest_nice']; + + // check processor usage + new FileModule.File('/proc/stat').read("\n").then(lines => { + let statistics = {}; + + for (let line of lines) { + let reverse_data = line.match(/^(cpu\d*\s)(.+)/); + if (reverse_data) { + let cpu = reverse_data[1].trim(); + + if (!(cpu in statistics)) + statistics[cpu] = {}; + + if (!(cpu in this._last_processor['core'])) + this._last_processor['core'][cpu] = 0; + + let stats = reverse_data[2].trim().split(' ').reverse(); + for (let column of columns) + statistics[cpu][column] = parseInt(stats.pop()); + } + } + + let cores = Object.keys(statistics).length - 1; + + for (let cpu in statistics) { + let total = statistics[cpu]['user'] + statistics[cpu]['nice'] + statistics[cpu]['system']; + + // make sure we have data to report + if (this._last_processor['core'][cpu] > 0) { + let delta = (total - this._last_processor['core'][cpu]) / dwell; + + // /proc/stat provides overall usage for us under the 'cpu' heading + if (cpu == 'cpu') { + delta = delta / cores; + this._returnValue(callback, 'processor', delta / 100, 'processor-group', 'percent'); + this._returnValue(callback, 'Usage', delta / 100, 'processor', 'percent'); + } else { + this._returnValue(callback, _('Core %d').format(cpu.substr(3)), delta / 100, 'processor', 'percent'); + } + } + + this._last_processor['core'][cpu] = total; + } + + // if frequency scaling is enabled, gather cpu-freq values + if (!this._processor_uses_cpu_info) { + for (let core = 0; core <= cores; core++) { + new FileModule.File('/sys/devices/system/cpu/cpu' + core + '/cpufreq/scaling_cur_freq').read().then(value => { + this._last_processor['speed'][core] = parseInt(value); + }).catch(err => { }); + } + } + }).catch(err => { }); + + // if frequency scaling is disabled, use cpuinfo for speed + if (this._processor_uses_cpu_info) { + // grab CPU frequency + new FileModule.File('/proc/cpuinfo').read("\n").then(lines => { + let freqs = []; + for (let line of lines) { + // grab megahertz + let value = line.match(/^cpu MHz(\s+): ([+-]?\d+(\.\d+)?)/); + if (value) freqs.push(parseFloat(value[2])); + } + + let sum = freqs.reduce((a, b) => a + b); + let hertz = (sum / freqs.length) * 1000 * 1000; + this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz'); + + //let max_hertz = Math.getMaxOfArray(freqs) * 1000 * 1000; + //this._returnValue(callback, 'Boost', max_hertz, 'processor', 'hertz'); + }).catch(err => { }); + // if frequency scaling is enabled, cpu-freq reports + } else if (Object.values(this._last_processor['speed']).length > 0) { + let sum = this._last_processor['speed'].reduce((a, b) => a + b); + let hertz = (sum / this._last_processor['speed'].length) * 1000; + this._returnValue(callback, 'Frequency', hertz, 'processor', 'hertz'); + //let max_hertz = Math.getMaxOfArray(this._last_processor['speed']) * 1000; + //this._returnValue(callback, 'Boost', max_hertz, 'processor', 'hertz'); + } + } + + _querySystem(callback) { + // check load average + new FileModule.File('/proc/sys/fs/file-nr').read("\t").then(loadArray => { + this._returnValue(callback, 'Open Files', loadArray[0], 'system', 'string'); + }).catch(err => { }); + + // check load average + new FileModule.File('/proc/loadavg').read(' ').then(loadArray => { + let proc = loadArray[3].split('/'); + + this._returnValue(callback, 'Load 1m', loadArray[0], 'system', 'load'); + this._returnValue(callback, 'system', loadArray[0], 'system-group', 'load'); + this._returnValue(callback, 'Load 5m', loadArray[1], 'system', 'load'); + this._returnValue(callback, 'Load 15m', loadArray[2], 'system', 'load'); + this._returnValue(callback, 'Threads Active', proc[0], 'system', 'string'); + this._returnValue(callback, 'Threads Total', proc[1], 'system', 'string'); + }).catch(err => { }); + + // check uptime + new FileModule.File('/proc/uptime').read(' ').then(upArray => { + this._returnValue(callback, 'Uptime', upArray[0], 'system', 'uptime'); + + let cores = Object.keys(this._last_processor['core']).length - 1; + if (cores > 0) + this._returnValue(callback, 'Process Time', upArray[0] - upArray[1] / cores, 'processor', 'uptime'); + }).catch(err => { }); + } + + _queryNetwork(callback, dwell) { + // check network speed + let directions = ['tx', 'rx']; + let netbase = '/sys/class/net/'; + + new FileModule.File(netbase).list().then(interfaces => { + for (let iface of interfaces) { + for (let direction of directions) { + // lo tx and rx are the same + if (iface == 'lo' && direction == 'rx') continue; + + new FileModule.File(netbase + iface + '/statistics/' + direction + '_bytes').read().then(value => { + // issue #217 - don't include 'lo' traffic in Maximum calculations in values.js + // by not using network-rx or network-tx + let name = iface + ((iface == 'lo')?'':' ' + direction); + + let type = 'network' + ((iface=='lo')?'':'-' + direction); + this._returnValue(callback, name, value, type, 'storage'); + }).catch(err => { }); + } + } + }).catch(err => { }); + + // some may not want public ip checking + if (this._settings.get_boolean('include-public-ip')) { + // check the public ip every hour or when waking from sleep + if (this._next_public_ip_check <= 0) { + this._next_public_ip_check = 3600; + + this._refreshIPAddress(callback); + } + + this._next_public_ip_check -= dwell; + } + + // wireless interface statistics + new FileModule.File('/proc/net/wireless').read("\n", true).then(lines => { + // wireless has two headers - first is stripped in helper function + lines.shift(); + + // if multiple wireless device, we use the last one + for (let line of lines) { + let netArray = line.trim().split(/\s+/); + let quality_pct = netArray[2].substr(0, netArray[2].length-1) / 70; + let signal = netArray[3].substr(0, netArray[3].length-1); + + this._returnValue(callback, 'WiFi Link Quality', quality_pct, 'network', 'percent'); + this._returnValue(callback, 'WiFi Signal Level', signal, 'network', 'string'); + } + }).catch(err => { }); + } + + _queryStorage(callback, dwell) { + // display zfs arc status, if available + new FileModule.File('/proc/spl/kstat/zfs/arcstats').read().then(lines => { + let values = '', target = 0, maximum = 0, current = 0; + + if (values = lines.match(/c(\s+)(\d+)(\s+)(\d+)/)) target = values[4]; + if (values = lines.match(/c_max(\s+)(\d+)(\s+)(\d+)/)) maximum = values[4]; + if (values = lines.match(/size(\s+)(\d+)(\s+)(\d+)/)) current = values[4]; + + // ZFS statistics + this._returnValue(callback, 'ARC Target', target, 'storage', 'storage'); + this._returnValue(callback, 'ARC Maximum', maximum, 'storage', 'storage'); + this._returnValue(callback, 'ARC Current', current, 'storage', 'storage'); + }).catch(err => { }); + + // check disk performance stats + new FileModule.File('/proc/diskstats').read("\n").then(lines => { + for (let line of lines) { + let loadArray = line.trim().split(/\s+/); + if ('/dev/' + loadArray[2] == this._storageDevice) { + var read = (loadArray[5] * 512); + var write = (loadArray[9] * 512); + this._returnValue(callback, 'Read total', read, 'storage', 'storage'); + this._returnValue(callback, 'Write total', write, 'storage', 'storage'); + this._returnValue(callback, 'Read rate', (read - this._lastRead) / dwell, 'storage', 'storage'); + this._returnValue(callback, 'Write rate', (write - this._lastWrite) / dwell, 'storage', 'storage'); + this._lastRead = read; + this._lastWrite = write; + break; + } + } + }).catch(err => { }); + + // skip rest of stats if gtop not available + if (!hasGTop) return; + + GTop.glibtop_get_fsusage(this.storage, this._settings.get_string('storage-path')); + + let total = this.storage.blocks * this.storage.block_size; + let avail = this.storage.bavail * this.storage.block_size; + let free = this.storage.bfree * this.storage.block_size; + let used = total - free; + let reserved = (total - avail) - used; + + this._returnValue(callback, 'Total', total, 'storage', 'storage'); + this._returnValue(callback, 'Used', used, 'storage', 'storage'); + this._returnValue(callback, 'Reserved', reserved, 'storage', 'storage'); + this._returnValue(callback, 'Free', avail, 'storage', 'storage'); + this._returnValue(callback, 'storage', avail, 'storage-group', 'storage'); + } + + _queryBattery(callback) { + let battery_slot = this._settings.get_int('battery-slot'); + + // addresses issue #161 + let battery_key = 'BAT'; // BAT0, BAT1 and BAT2 + if (battery_slot == 3) { + battery_key = 'CMB'; // CMB0 + battery_slot = 0; + } else if (battery_slot == 4) { + battery_key = 'macsmc-battery'; // supports Asahi linux + battery_slot = ''; + } + + // uevent has all necessary fields, no need to read individual files + let battery_path = '/sys/class/power_supply/' + battery_key + battery_slot + '/uevent'; + new FileModule.File(battery_path).read("\n").then(lines => { + let output = {}; + for (let line of lines) { + let split = line.split('='); + output[split[0].replace('POWER_SUPPLY_', '')] = split[1]; + } + + if ('STATUS' in output) { + this._returnValue(callback, 'State', output['STATUS'], 'battery', ''); + } + + if ('CYCLE_COUNT' in output) { + this._returnValue(callback, 'Cycles', output['CYCLE_COUNT'], 'battery', ''); + } + + if ('VOLTAGE_NOW' in output) { + this._returnValue(callback, 'Voltage', output['VOLTAGE_NOW'] / 1000, 'battery', 'in'); + } + + if ('CAPACITY_LEVEL' in output) { + this._returnValue(callback, 'Level', output['CAPACITY_LEVEL'], 'battery', ''); + } + + if ('CAPACITY' in output) { + this._returnValue(callback, 'Percentage', output['CAPACITY'] / 100, 'battery', 'percent'); + } + + if ('VOLTAGE_NOW' in output && 'CURRENT_NOW' in output && (!('POWER_NOW' in output))) { + output['POWER_NOW'] = (output['VOLTAGE_NOW'] * output['CURRENT_NOW']) / 1000000; + } + + if ('POWER_NOW' in output) { + this._returnValue(callback, 'Rate', output['POWER_NOW'], 'battery', 'watt'); + this._returnValue(callback, 'battery', output['POWER_NOW'], 'battery-group', 'watt'); + } + + if ('CHARGE_FULL' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL' in output))) { + output['ENERGY_FULL'] = (output['CHARGE_FULL'] * output['VOLTAGE_MIN_DESIGN']) / 1000000; + } + + if ('ENERGY_FULL' in output) { + this._returnValue(callback, 'Energy (full)', output['ENERGY_FULL'], 'battery', 'watt-hour'); + } + + if ('CHARGE_FULL_DESIGN' in output && 'VOLTAGE_MIN_DESIGN' in output && (!('ENERGY_FULL_DESIGN' in output))) { + output['ENERGY_FULL_DESIGN'] = (output['CHARGE_FULL_DESIGN'] * output['VOLTAGE_MIN_DESIGN']) / 1000000; + } + + if ('ENERGY_FULL_DESIGN' in output) { + this._returnValue(callback, 'Energy (design)', output['ENERGY_FULL_DESIGN'], 'battery', 'watt-hour'); + + if ('ENERGY_FULL' in output) { + this._returnValue(callback, 'Capacity', (output['ENERGY_FULL'] / output['ENERGY_FULL_DESIGN']), 'battery', 'percent'); + } + } + + if ('VOLTAGE_MIN_DESIGN' in output && 'CHARGE_NOW' in output && (!('ENERGY_NOW' in output))) { + output['ENERGY_NOW'] = (output['VOLTAGE_MIN_DESIGN'] * output['CHARGE_NOW']) / 1000000; + } + + if ('ENERGY_NOW' in output) { + this._returnValue(callback, 'Energy (now)', output['ENERGY_NOW'], 'battery', 'watt-hour'); + } + + if ('ENERGY_FULL' in output && 'ENERGY_NOW' in output && 'POWER_NOW' in output && output['POWER_NOW'] > 0 && 'STATUS' in output && (output['STATUS'] == 'Charging' || output['STATUS'] == 'Discharging')) { + + let timeLeft = 0; + + // two different formulas depending on if we are charging or discharging + if (output['STATUS'] == 'Charging') { + timeLeft = ((output['ENERGY_FULL'] - output['ENERGY_NOW']) / output['POWER_NOW']); + } else { + timeLeft = (output['ENERGY_NOW'] / output['POWER_NOW']); + } + + // don't process Infinity values + if (timeLeft !== Infinity) { + if (this._battery_charge_status != output['STATUS']) { + // clears history due to state change + this._battery_time_left_history = []; + + // clear time left history when laptop goes in and out of charging + this._battery_charge_status = output['STATUS']; + } + + // add latest time left estimate to our history + this._battery_time_left_history.push(parseInt(timeLeft * 3600)); + + // keep track of last 15 time left estimates by erasing the first + if (this._battery_time_left_history.length > 10) + this._battery_time_left_history.shift(); + + // sum up and create average of our time left history + let sum = this._battery_time_left_history.reduce((a, b) => a + b); + let avg = sum / this._battery_time_left_history.length; + + // use time left history to update screen + this._returnValue(callback, 'Time left', parseInt(avg), 'battery', 'runtime'); + } + } else { + this._returnValue(callback, 'Time left', output['STATUS'], 'battery', ''); + } + }).catch(err => { }); + } + + _returnValue(callback, label, value, type, format) { + // don't return if value is not a number - will revisit later + //if (isNaN(value)) return; + callback(label, value, type, format); + } + + _discoverHardwareMonitors(callback) { + this._tempVoltFanSensors = { 'temperature': {}, 'voltage': {}, 'fan': {} }; + + let hwbase = '/sys/class/hwmon/'; + + // process sensor_types now so it is not called multiple times below + let sensor_types = {}; + + if (this._settings.get_boolean('show-temperature')) + sensor_types['temp'] = 'temperature'; + + if (this._settings.get_boolean('show-voltage')) + sensor_types['in'] = 'voltage'; + + if (this._settings.get_boolean('show-fan')) + sensor_types['fan'] = 'fan'; + + // a little informal, but this code has zero I/O block + new FileModule.File(hwbase).list().then(files => { + for (let file of files) { + // grab name of sensor + new FileModule.File(hwbase + file + '/name').read().then(name => { + // are we dealing with a CPU? + if (name == 'coretemp') { + // determine which processor (socket) we are dealing with + new FileModule.File(hwbase + file + '/temp1_label').read().then(prefix => { + this._processTempVoltFan(callback, sensor_types, prefix, hwbase + file, file); + }).catch(err => { + // this shouldn't be necessary, but just in case temp1_label doesn't exist + // attempt to fix #266 + this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file); + }); + } else { + // not a CPU, process all other sensors + this._processTempVoltFan(callback, sensor_types, name, hwbase + file, file); + } + }).catch(err => { + new FileModule.File(hwbase + file + '/device/name').read().then(name => { + this._processTempVoltFan(callback, sensor_types, name, hwbase + file + '/device', file); + }).catch(err => { }); + }); + } + }).catch(err => { }); + + // does this system support cpu scaling? if so we will use it to grab Frequency and Boost below + new FileModule.File('/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq').read().then(value => { + this._processor_uses_cpu_info = false; + }).catch(err => { }); + + // is static CPU information enabled? + if (this._settings.get_boolean('include-static-info')) { + // grab static CPU information + new FileModule.File('/proc/cpuinfo').read("\n").then(lines => { + let vendor_id = ''; + let bogomips = ''; + let sockets = {}; + let cache = ''; + + for (let line of lines) { + let value = ''; + + // grab cpu vendor + if (value = line.match(/^vendor_id(\s+): (\w+.*)/)) vendor_id = value[2]; + + // grab bogomips + if (value = line.match(/^bogomips(\s+): (\d*\.?\d*)$/)) bogomips = value[2]; + + // grab processor count + if (value = line.match(/^physical id(\s+): (\d+)$/)) sockets[value[2]] = 1; + + // grab cache + if (value = line.match(/^cache size(\s+): (\d+) KB$/)) cache = value[2]; + } + + this._returnValue(callback, 'Vendor', vendor_id, 'processor', 'string'); + this._returnValue(callback, 'Bogomips', bogomips, 'processor', 'string'); + this._returnValue(callback, 'Sockets', Object.keys(sockets).length, 'processor', 'string'); + this._returnValue(callback, 'Cache', cache, 'processor', 'memory'); + }).catch(err => { }); + + // grab static CPU information + new FileModule.File('/proc/version').read(' ').then(kernelArray => { + this._returnValue(callback, 'Kernel', kernelArray[2], 'system', 'string'); + }).catch(err => { }); + } + } + + _processTempVoltFan(callback, sensor_types, name, path, file) { + let sensor_files = [ 'input', 'label' ]; + + // grab files from directory + new FileModule.File(path).list().then(files2 => { + let trisensors = {}; + + // loop over files from directory + for (let file2 of Object.values(files2)) { + // simple way of processing input and label (from above) + for (let key of Object.values(sensor_files)) { + // process toggled on sensors from extension preferences + for (let sensor_type in sensor_types) { + if (file2.substr(0, sensor_type.length) == sensor_type && file2.substr(-(key.length+1)) == '_' + key) { + let key2 = file + file2.substr(0, file2.indexOf('_')); + + if (!(key2 in trisensors)) { + trisensors[key2] = { + 'type': sensor_types[sensor_type], + 'format': sensor_type, + 'label': path + '/name' + }; + } + + trisensors[key2][key] = path + '/' + file2; + } + } + } + } + + for (let obj of Object.values(trisensors)) { + if (!('input' in obj)) + continue; + + new FileModule.File(obj['input']).read().then(value => { + let extra = (obj['label'].indexOf('_label')==-1) ? ' ' + obj['input'].substr(obj['input'].lastIndexOf('/')+1).split('_')[0] : ''; + + if (value > 0 || !this._settings.get_boolean('hide-zeros') || obj['type'] == 'fan') { + new FileModule.File(obj['label']).read().then(label => { + this._addTempVoltFan(callback, obj, name, label, extra, value); + }).catch(err => { + let tmpFile = obj['label'].substr(0, obj['label'].lastIndexOf('/')) + '/name'; + new FileModule.File(tmpFile).read().then(label => { + this._addTempVoltFan(callback, obj, name, label, extra, value); + }).catch(err => { }); + }); + } + }).catch(err => { }); + } + }).catch(err => { }); + } + + _addTempVoltFan(callback, obj, name, label, extra, value) { + // prepend module that provided sensor data + if (name != label) label = name + ' ' + label; + + //if (label == 'nvme Composite') label = 'NVMe'; + //if (label == 'nouveau') label = 'Nvidia'; + + label = label + extra; + + // in the future we will read /etc/sensors3.conf + if (label == 'acpitz temp1') label = 'ACPI Thermal Zone'; + if (label == 'pch_cannonlake temp1') label = 'Platform Controller Hub'; + if (label == 'iwlwifi_1 temp1') label = 'Wireless Adapter'; + if (label == 'Package id 0') label = 'Processor 0'; + if (label == 'Package id 1') label = 'Processor 1'; + label = label.replace('Package id', 'CPU'); + + let types = [ 'temperature', 'voltage', 'fan' ]; + for (let type of types) { + // check if this label already exists + if (label in this._tempVoltFanSensors[type]) { + for (let i = 2; i <= 9; i++) { + // append an incremented number to end + let new_label = label + ' ' + i; + + // if new label is available, use it + if (!(new_label in this._tempVoltFanSensors[type])) { + label = new_label; + break; + } + } + } + } + + // update screen on initial build to prevent delay on update + this._returnValue(callback, label, value, obj['type'], obj['format']); + + this._tempVoltFanSensors[obj['type']][label] = { + 'format': obj['format'], + 'path': obj['input'] + }; + } + + resetHistory() { + this._next_public_ip_check = 0; + this._hardware_detected = false; + this._processor_uses_cpu_info = true; + this._battery_time_left_history = []; + this._battery_charge_status = ''; + } +}); diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/stylesheet.css b/gnome/.local/share/extensions/Vitals@CoreCoding.com/stylesheet.css new file mode 100644 index 0000000..adec425 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/stylesheet.css @@ -0,0 +1,16 @@ +.vitals-icon { icon-size: 16px; } +.vitals-menu-button-container {} +.vitals-panel-icon-temperature { margin: 0 1px 0 8px; padding: 0; } +.vitals-panel-icon-voltage { margin: 0 0 0 8px; padding: 0; } +.vitals-panel-icon-fan { margin: 0 4px 0 8px; padding: 0; } +.vitals-panel-icon-memory { margin: 0 2px 0 8px; padding: 0; } +.vitals-panel-icon-processor { margin: 0 3px 0 8px; padding: 0; } +.vitals-panel-icon-system { margin: 0 3px 0 8px; padding: 0; } +.vitals-panel-icon-network { margin: 0 3px 0 8px; padding: 0; } +.vitals-panel-icon-storage { margin: 0 2px 0 8px; padding: 0; } +.vitals-panel-icon-battery { margin: 0 4px 0 8px; padding: 0; } +.vitals-panel-label { margin: 0 3px 0 0; padding: 0; } +.vitals-button-action { -st-icon-style: symbolic; border-radius: 32px; margin: 0px; min-height: 22px; min-width: 22px; padding: 10px; font-size: 100%; border: 1px solid transparent; } +.vitals-button-action:hover, .vitals-button-action:focus { border-color: #777; } +.vitals-button-action > StIcon { icon-size: 16px; } +.vitals-button-box { padding: 0px; spacing: 22px; } diff --git a/gnome/.local/share/extensions/Vitals@CoreCoding.com/values.js b/gnome/.local/share/extensions/Vitals@CoreCoding.com/values.js new file mode 100644 index 0000000..77c47f1 --- /dev/null +++ b/gnome/.local/share/extensions/Vitals@CoreCoding.com/values.js @@ -0,0 +1,337 @@ +/* + Copyright (c) 2018, Chris Monahan + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the GNOME nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +import GObject from 'gi://GObject'; + +const cbFun = (d, c) => { + let bb = d[1] % c[0], + aa = (d[1] - bb) / c[0]; + aa = aa > 0 ? aa + c[1] : ''; + + return [d[0] + aa, bb]; +}; + +export const Values = GObject.registerClass({ + GTypeName: 'Values', +}, class Values extends GObject.Object { + + _init(settings, sensorIcons) { + this._settings = settings; + this._sensorIcons = sensorIcons; + + this._networkSpeedOffset = {}; + this._networkSpeeds = {}; + + this._history = {}; + //this._history2 = {}; + this.resetHistory(); + } + + _legible(value, sensorClass) { + let unit = 1000; + if (value === null) return 'N/A'; + let use_higher_precision = this._settings.get_boolean('use-higher-precision'); + let memory_measurement = this._settings.get_int('memory-measurement') + let storage_measurement = this._settings.get_int('storage-measurement') + let use_bps = (this._settings.get_int('network-speed-format') == 1); + + let format = ''; + let ending = ''; + let exp = 0; + + var decimal = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ]; + var binary = [ 'B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB' ]; + var hertz = [ 'Hz', 'KHz', 'MHz', 'GHz', 'THz', 'PHz', 'EHz', 'ZHz' ]; + + switch (sensorClass) { + case 'percent': + format = (use_higher_precision)?'%.1f%s':'%d%s'; + value = value * 100; + if (value > 100) value = 100; + ending = '%'; + break; + case 'temp': + value = value / 1000; + ending = '°C'; + + // are we converting to fahrenheit? + if (this._settings.get_int('unit') == 1) { + value = ((9 / 5) * value + 32); + ending = '°F'; + } + + format = (use_higher_precision)?'%.1f%s':'%d%s'; + break; + case 'fan': + format = '%d %s'; + ending = 'RPM'; + break; + case 'in': // voltage + value = value / 1000; + format = ((value >= 0) ? '+' : '-') + ((use_higher_precision)?'%.2f %s':'%.1f %s'); + ending = 'V'; + break; + case 'hertz': + if (value > 0) { + exp = Math.floor(Math.log(value) / Math.log(unit)); + if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++; + value = parseFloat((value / Math.pow(unit, exp))); + } + + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + ending = hertz[exp]; + break; + case 'memory': + unit = (memory_measurement)?1000:1024; + + if (value > 0) { + value *= unit; + exp = Math.floor(Math.log(value) / Math.log(unit)); + if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++; + value = parseFloat((value / Math.pow(unit, exp))); + } + + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + + if (memory_measurement) + ending = decimal[exp]; + else + ending = binary[exp]; + + break; + case 'storage': + unit = (storage_measurement)?1000:1024; + + if (value > 0) { + exp = Math.floor(Math.log(value) / Math.log(unit)); + if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++; + value = parseFloat((value / Math.pow(unit, exp))); + } + + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + + if (storage_measurement) + ending = decimal[exp]; + else + ending = binary[exp]; + + break; + case 'speed': + if (value > 0) { + if (use_bps) value *= 8; + exp = Math.floor(Math.log(value) / Math.log(unit)); + if (value >= Math.pow(unit, exp) * (unit - 0.05)) exp++; + value = parseFloat((value / Math.pow(unit, exp))); + } + + format = (use_higher_precision)?'%.1f %s':'%.0f %s'; + + if (use_bps) { + ending = decimal[exp].replace('B', 'bps'); + } else { + ending = decimal[exp] + '/s'; + } + + break; + case 'runtime': + case 'uptime': + let scale = [24, 60, 60]; + let units = ['d ', 'h ', 'm ']; + + // show seconds on higher precision or if value under a minute + if (sensorClass != 'runtime' && (use_higher_precision || value < 60)) { + scale.push(1); + units.push('s '); + } + + let rslt = scale.map((d, i, a) => a.slice(i).reduce((d, c) => d * c)) + .map((d, i) => ([d, units[i]])) + .reduce(cbFun, ['', value]); + + value = rslt[0].trim(); + + format = '%s'; + break; + case 'milliamp': + format = (use_higher_precision)?'%.1f %s':'%d %s'; + value = value / 1000; + ending = 'mA'; + break; + case 'milliamp-hour': + format = (use_higher_precision)?'%.1f %s':'%d %s'; + value = value / 1000; + ending = 'mAh'; + break; + case 'watt': + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + value = value / 1000000; + ending = 'W'; + break; + case 'watt-hour': + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + value = value / 1000000; + ending = 'Wh'; + break; + case 'load': + format = (use_higher_precision)?'%.2f %s':'%.1f %s'; + break; + default: + format = '%s'; + break; + } + + return format.format(value, ending); + } + + returnIfDifferent(dwell, label, value, type, format, key) { + let output = []; + + // make sure the keys exist + if (!(type in this._history)) this._history[type] = {}; + + // no sense in continuing when the raw value has not changed + if (type != 'network-rx' && type != 'network-tx' && + key in this._history[type] && this._history[type][key][1] == value) + return output; + + // is the value different from last time? + let legible = this._legible(value, format); + + // don't return early when dealing with network traffic + if (type != 'network-rx' && type != 'network-tx') { + // only update when we are coming through for the first time, or if a value has changed + if (key in this._history[type] && this._history[type][key][0] == legible) + return output; + + // add label as it was sent from sensors class + output.push([label, legible, type, key]); + } + + // save previous values to update screen on changes only + let previousValue = this._history[type][key]; + this._history[type][key] = [legible, value]; + + // process average, min and max values + if (type == 'temperature' || type == 'voltage' || type == 'fan') { + let vals = Object.values(this._history[type]).map(x => parseFloat(x[1])); + + // show value in group even if there is one value present + let sum = vals.reduce((a, b) => a + b); + let avg = this._legible(sum / vals.length, format); + output.push([type, avg, type + '-group', '']); + + // If only one value is present, don't display avg, min and max + if (vals.length > 1) { + output.push(['Average', avg, type, '__' + type + '_avg__']); + + // calculate Minimum value + let min = Math.min(...vals); + min = this._legible(min, format); + output.push(['Minimum', min, type, '__' + type + '_min__']); + + // calculate Maximum value + let max = Math.max(...vals); + max = this._legible(max, format); + output.push(['Maximum', max, type, '__' + type + '_max__']); + } + } else if (type == 'network-rx' || type == 'network-tx') { + let direction = type.split('-')[1]; + + // appends total upload and download for all interfaces for #216 + let vals = Object.values(this._history[type]).map(x => parseFloat(x[1])); + let sum = vals.reduce((partialSum, a) => partialSum + a, 0); + output.push(['Boot ' + direction, this._legible(sum, format), type, '__' + type + '_boot__']); + + // keeps track of session start point + if (!(key in this._networkSpeedOffset) || this._networkSpeedOffset[key] <= 0) + this._networkSpeedOffset[key] = sum; + + // outputs session upload and download for all interfaces for #234 + output.push(['Session ' + direction, this._legible(sum - this._networkSpeedOffset[key], format), type, '__' + type + '_ses__']); + + // calculate speed for this interface + let speed = (value - previousValue[1]) / dwell; + output.push([label, this._legible(speed, 'speed'), type, key]); + + // store speed for Device report + if (!(direction in this._networkSpeeds)) this._networkSpeeds[direction] = {}; + if (!(label in this._networkSpeeds[direction])) this._networkSpeeds[direction][label] = 0; + + // store value for next go around + if (value > 0 || (value == 0 && !this._settings.get_boolean('hide-zeros'))) + this._networkSpeeds[direction][label] = speed; + + // calculate total upload and download device speed + for (let direction in this._networkSpeeds) { + let sum = 0; + for (let iface in this._networkSpeeds[direction]) + sum += parseFloat(this._networkSpeeds[direction][iface]); + + sum = this._legible(sum, 'speed'); + output.push(['Device ' + direction, sum, 'network-' + direction, '__network-' + direction + '_max__']); + // append download speed to group itself + if (direction == 'rx') output.push([type, sum, type + '-group', '']); + } + } + +/* + global.log('before', JSON.stringify(output)); + for (let i = output.length - 1; i >= 0; i--) { + let sensor = output[i]; + // sensor[0]=label, sensor[1]=value, sensor[2]=type, sensor[3]=key) + + //["CPU Core 5","46°C","temperature","_temperature_hwmon8temp7_"] + + // make sure the keys exist + if (!(sensor[2] in this._history2)) this._history2[sensor[2]] = {}; + + if (sensor[3] in this._history2[sensor[2]]) { + if (this._history2[sensor[2]][sensor[3]] == sensor[1]) { + output.splice(i, 1); + } + } + + this._history2[sensor[2]][sensor[3]] = sensor[1]; + } + + global.log(' after', JSON.stringify(output)); + global.log('***************************'); +*/ + + return output; + } + + resetHistory() { + // don't call this._history = {}, as we want to keep network-rx and network-tx + // otherwise network history statistics will start over + for (let sensor in this._sensorIcons) { + this._history[sensor] = {}; + this._history[sensor + '-group'] = {}; + //this._history2[sensor] = {}; + //this._history2[sensor + '-group'] = {}; + } + } +}); diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js new file mode 100644 index 0000000..bbcaac6 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/appIndicator.js @@ -0,0 +1,1571 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import GdkPixbuf from 'gi://GdkPixbuf'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; + +import * as Params from 'resource:///org/gnome/shell/misc/params.js'; +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +import * as IconCache from './iconCache.js'; +import * as Util from './util.js'; +import * as Interfaces from './interfaces.js'; +import * as PixmapsUtils from './pixmapsUtils.js'; +import * as PromiseUtils from './promiseUtils.js'; +import * as SettingsManager from './settingsManager.js'; +import {DBusProxy} from './dbusProxy.js'; + +Gio._promisify(Gio.File.prototype, 'read_async'); +Gio._promisify(GdkPixbuf.Pixbuf, 'get_file_info_async'); +Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_at_scale_async', + 'new_from_stream_finish'); +Gio._promisify(St.IconInfo.prototype, 'load_symbolic_async'); + +const MAX_UPDATE_FREQUENCY = 30; // In ms +const FALLBACK_ICON_NAME = 'image-loading-symbolic'; +const PIXMAPS_FORMAT = imports.gi.Cogl.PixelFormat.ARGB_8888; + +export const SNICategory = Object.freeze({ + APPLICATION: 'ApplicationStatus', + COMMUNICATIONS: 'Communications', + SYSTEM: 'SystemServices', + HARDWARE: 'Hardware', +}); + +export const SNIStatus = Object.freeze({ + PASSIVE: 'Passive', + ACTIVE: 'Active', + NEEDS_ATTENTION: 'NeedsAttention', +}); + +const SNIconType = Object.freeze({ + NORMAL: 0, + ATTENTION: 1, + OVERLAY: 2, + + toPropertyName: (iconType, params = {isPixbuf: false}) => { + let propertyName = 'Icon'; + + if (iconType === SNIconType.OVERLAY) + propertyName = 'OverlayIcon'; + else if (iconType === SNIconType.ATTENTION) + propertyName = 'AttentionIcon'; + + return `${propertyName}${params.isPixbuf ? 'Pixmap' : 'Name'}`; + }, +}); + +export const AppIndicatorProxy = GObject.registerClass( +class AppIndicatorProxy extends DBusProxy { + static get interfaceInfo() { + if (!this._interfaceInfo) { + this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml( + Interfaces.StatusNotifierItem); + } + return this._interfaceInfo; + } + + static get OPTIONAL_PROPERTIES() { + return [ + 'XAyatanaLabel', + 'XAyatanaLabelGuide', + 'XAyatanaOrderingIndex', + 'IconAccessibleDesc', + 'AttentionAccessibleDesc', + ]; + } + + static get TUPLE_TYPE() { + if (!this._tupleType) + this._tupleType = new GLib.VariantType('()'); + + return this._tupleType; + } + + static destroy() { + delete this._interfaceInfo; + delete this._tupleType; + } + + _init(busName, objectPath) { + const {interfaceInfo} = AppIndicatorProxy; + + super._init(busName, objectPath, interfaceInfo, + Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES); + + this.set_cached_property('Status', + new GLib.Variant('s', SNIStatus.PASSIVE)); + + + this._accumulatedProperties = new Set(); + this._cancellables = new Map(); + this._changedProperties = Object.create(null); + } + + async initAsync(cancellable) { + await super.initAsync(cancellable); + + this._setupProxyPropertyList(); + } + + destroy() { + const cachedProperties = this.get_cached_property_names(); + if (cachedProperties) { + cachedProperties.forEach(propertyName => + this.set_cached_property(propertyName, null)); + } + + super.destroy(); + } + + _onNameOwnerChanged() { + this._resetNeededProperties(); + + if (!this.gNameOwner) + this._cancelRefreshProperties(); + else + this._setupProxyPropertyList(); + } + + _setupProxyPropertyList() { + this._propertiesList = + (this.get_cached_property_names() || []).filter(p => + this.gInterfaceInfo.properties.some(pInfo => pInfo.name === p)); + + if (this._propertiesList.length) { + AppIndicatorProxy.OPTIONAL_PROPERTIES.forEach( + p => this._addExtraProperty(p)); + } + } + + _addExtraProperty(name) { + if (this._propertiesList.includes(name)) + return; + + if (!(name in this)) { + Object.defineProperty(this, name, { + configurable: false, + enumerable: true, + get: () => { + const v = this.get_cached_property(name); + return v ? v.deep_unpack() : null; + }, + }); + } + + this._propertiesList.push(name); + } + + _signalToPropertyName(signal) { + if (signal.startsWith('New')) + return signal.substr(3); + else if (signal.startsWith('XAyatanaNew')) + return `XAyatana${signal.substr(11)}`; + + return null; + } + + // The Author of the spec didn't like the PropertiesChanged signal, so he invented his own + async _refreshOwnProperties(prop) { + await Promise.all( + [prop, `${prop}Name`, `${prop}Pixmap`, `${prop}AccessibleDesc`].filter(p => + this._propertiesList.includes(p)).map(async p => { + try { + await this.refreshProperty(p, { + skipEqualityCheck: p.endsWith('Pixmap'), + }); + } catch (e) { + if (!AppIndicatorProxy.OPTIONAL_PROPERTIES.includes(p) || + !e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_PROPERTY)) + logError(e); + } + })); + } + + _onSignal(...args) { + this._onSignalAsync(...args).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + } + + async _onSignalAsync(_sender, signal, params) { + const property = this._signalToPropertyName(signal); + if (!property) + return; + + if (this.status === SNIStatus.PASSIVE && + ![...AppIndicator.NEEDED_PROPERTIES, 'Status'].includes(property)) { + this._accumulatedProperties.add(property); + return; + } + + if (!params.get_type().equal(AppIndicatorProxy.TUPLE_TYPE)) { + // If the property includes arguments, we can just queue the signal emission + const [value] = params.unpack(); + try { + await this._queuePropertyUpdate(property, value); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; + } + + if (!this._accumulatedProperties.size) + return; + } else { + this._accumulatedProperties.add(property); + } + + if (this._signalsAccumulator) + return; + + this._signalsAccumulator = new PromiseUtils.TimeoutPromise( + MAX_UPDATE_FREQUENCY, GLib.PRIORITY_DEFAULT_IDLE, this._cancellable); + try { + await this._signalsAccumulator; + const refreshPropertiesPromises = + [...this._accumulatedProperties].map(p => + this._refreshOwnProperties(p)); + this._accumulatedProperties.clear(); + await Promise.all(refreshPropertiesPromises); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; + } finally { + delete this._signalsAccumulator; + } + } + + _resetNeededProperties() { + AppIndicator.NEEDED_PROPERTIES.forEach(p => + this.set_cached_property(p, null)); + } + + async refreshAllProperties() { + const cancellableName = 'org.freedesktop.DBus.Properties.GetAll'; + const cancellable = this._cancelRefreshProperties({ + propertyName: cancellableName, + addNew: true, + }); + + try { + const [valuesVariant] = (await this.getProperties( + cancellable)).deep_unpack(); + + this._cancellables.delete(cancellableName); + + await Promise.all( + Object.entries(valuesVariant).map(([propertyName, valueVariant]) => + this._queuePropertyUpdate(propertyName, valueVariant, { + skipEqualityCheck: true, + cancellable, + }))); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + // the property may not even exist, silently ignore it + Util.Logger.debug(`While refreshing all properties: ${e}`); + + this.get_cached_property_names().forEach(propertyName => + this.set_cached_property(propertyName, null)); + + this._cancellables.delete(cancellableName); + throw e; + } + } + } + + async refreshProperty(propertyName, params) { + params = Params.parse(params, { + skipEqualityCheck: false, + }); + + const cancellable = this._cancelRefreshProperties({ + propertyName, + addNew: true, + }); + + try { + const [valueVariant] = (await this.getProperty( + propertyName, cancellable)).deep_unpack(); + + this._cancellables.delete(propertyName); + await this._queuePropertyUpdate(propertyName, valueVariant, + Object.assign(params, {cancellable})); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + // the property may not even exist, silently ignore it + Util.Logger.debug(`While refreshing property ${propertyName}: ${e}`); + this.set_cached_property(propertyName, null); + this._cancellables.delete(propertyName); + delete this._changedProperties[propertyName]; + throw e; + } + } + } + + async _queuePropertyUpdate(propertyName, value, params) { + params = Params.parse(params, { + skipEqualityCheck: false, + cancellable: null, + }); + + if (!params.skipEqualityCheck) { + const cachedProperty = this.get_cached_property(propertyName); + + if (value && cachedProperty && + value.equal(this.get_cached_property(propertyName))) + return; + } + + this.set_cached_property(propertyName, value); + + // synthesize a batched property changed event + this._changedProperties[propertyName] = value; + + if (!this._propertiesEmitTimeout || !this._propertiesEmitTimeout.pending()) { + if (!params.cancellable) { + params.cancellable = this._cancelRefreshProperties({ + propertyName, + addNew: true, + }); + } + this._propertiesEmitTimeout = new PromiseUtils.TimeoutPromise( + MAX_UPDATE_FREQUENCY * 2, GLib.PRIORITY_DEFAULT_IDLE, params.cancellable); + await this._propertiesEmitTimeout; + + if (Object.keys(this._changedProperties).length) { + this.emit('g-properties-changed', GLib.Variant.new('a{sv}', + this._changedProperties), []); + this._changedProperties = Object.create(null); + } + } + } + + _cancelRefreshProperties(params) { + params = Params.parse(params, { + propertyName: undefined, + addNew: false, + }); + + if (!this._cancellables.size && !params.addNew) + return null; + + if (params.propertyName !== undefined) { + let cancellable = this._cancellables.get(params.propertyName); + if (cancellable) { + cancellable.cancel(); + + if (!params.addNew) + this._cancellables.delete(params.propertyName); + } + + if (params.addNew) { + cancellable = new Util.CancellableChild(this._cancellable); + this._cancellables.set(params.propertyName, cancellable); + return cancellable; + } + } else { + this._cancellables.forEach(c => c.cancel()); + this._cancellables.clear(); + this._changedProperties = Object.create(null); + } + + return null; + } +}); + +/** + * the AppIndicator class serves as a generic container for indicator information and functions common + * for every displaying implementation (IndicatorMessageSource and IndicatorStatusIcon) + */ +export class AppIndicator extends Signals.EventEmitter { + static get NEEDED_PROPERTIES() { + return ['Id', 'Menu']; + } + + constructor(service, busName, object) { + super(); + + this.isReady = false; + this.busName = busName; + this._uniqueId = Util.indicatorId(service, busName, object); + + this._cancellable = new Gio.Cancellable(); + this._proxy = new AppIndicatorProxy(busName, object); + this._invalidatedPixmapsIcons = new Set(); + + this._setupProxy().catch(logError); + Util.connectSmart(this._proxy, 'g-properties-changed', this, this._onPropertiesChanged); + Util.connectSmart(this._proxy, 'notify::g-name-owner', this, this._nameOwnerChanged); + + if (this.uniqueId === service) { + this._nameWatcher = new Util.NameWatcher(service); + Util.connectSmart(this._nameWatcher, 'changed', this, this._nameOwnerChanged); + } + } + + async _setupProxy() { + const cancellable = this._cancellable; + + try { + await this._proxy.initAsync(cancellable); + this._checkIfReady(); + await this._checkNeededProperties(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + logError(e, `While initalizing proxy for ${this._uniqueId}`); + this.destroy(); + } + } + + try { + this._commandLine = await Util.getProcessName(this.busName, + cancellable, GLib.PRIORITY_LOW); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + Util.Logger.debug( + `${this.uniqueId}, failed getting command line: ${e.message}`); + } + } + } + + _checkIfReady() { + const wasReady = this.isReady; + let isReady = false; + + if (this.hasNameOwner && this.id && this.menuPath) + isReady = true; + + this.isReady = isReady; + + if (this.isReady && !wasReady) { + if (this._delayCheck) { + this._delayCheck.cancel(); + delete this._delayCheck; + } + + this.emit('ready'); + return true; + } + + return false; + } + + async _checkNeededProperties() { + if (this.id && this.menuPath) + return true; + + const MAX_RETRIES = 3; + const cancellable = this._cancellable; + for (let checks = 0; checks < MAX_RETRIES; ++checks) { + this._delayCheck = new PromiseUtils.TimeoutSecondsPromise(1, + GLib.PRIORITY_DEFAULT_IDLE, cancellable); + // eslint-disable-next-line no-await-in-loop + await this._delayCheck; + + try { + // eslint-disable-next-line no-await-in-loop + await Promise.all(AppIndicator.NEEDED_PROPERTIES.map(p => + this._proxy.refreshProperty(p))); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; + + if (checks < MAX_RETRIES - 1) + continue; + + throw e; + } + + if (this.id && this.menuPath) + break; + } + + return this.id && this.menuPath; + } + + async _nameOwnerChanged() { + if (!this.hasNameOwner) { + this._checkIfReady(); + } else { + try { + await this._checkNeededProperties(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + Util.Logger.warn(`${this.uniqueId}, Impossible to get basic properties: ${e}`); + this.checkAlive(); + } + } + } + + this.emit('name-owner-changed'); + } + + // public property getters + get title() { + return this._proxy.Title; + } + + get id() { + return this._proxy.Id; + } + + get uniqueId() { + return this._uniqueId; + } + + get status() { + return this._proxy.Status; + } + + get label() { + return this._proxy.XAyatanaLabel || null; + } + + get accessibleName() { + const accessibleDesc = this.status === SNIStatus.NEEDS_ATTENTION + ? this._proxy.AccessibleDesc : this._proxy.IconAccessibleDesc; + + return accessibleDesc || this.title; + } + + get menuPath() { + if (this._proxy.Menu === '/NO_DBUSMENU') + return null; + + return this._proxy.Menu; + } + + get attentionIcon() { + return { + theme: this._proxy.IconThemePath, + name: this._proxy.AttentionIconName, + pixmap: this._getPixmapProperty(SNIconType.ATTENTION), + }; + } + + get icon() { + return { + theme: this._proxy.IconThemePath, + name: this._proxy.IconName, + pixmap: this._getPixmapProperty(SNIconType.NORMAL), + }; + } + + get overlayIcon() { + return { + theme: this._proxy.IconThemePath, + name: this._proxy.OverlayIconName, + pixmap: this._getPixmapProperty(SNIconType.OVERLAY), + }; + } + + get hasOverlayIcon() { + const {name, pixmap} = this.overlayIcon; + + return name || (pixmap && pixmap.n_children()); + } + + get hasNameOwner() { + if (this._nameWatcher && !this._nameWatcher.nameOnBus) + return false; + return !!this._proxy.g_name_owner; + } + + get cancellable() { + return this._cancellable; + } + + async checkAlive() { + // Some applications (hey electron!) just remove the indicator object + // from bus after hiding it, without closing its bus name, so we are + // not able to understand whe they're gone. + // Thus we just kill it when an expected well-known method is failing. + if (this.status !== SNIStatus.PASSIVE && this._checkIfReady()) { + if (this._checkAliveTimeout) { + this._checkAliveTimeout.cancel(); + delete this._checkAliveTimeout; + } + return; + } + + if (this._checkAliveTimeout) + return; + + try { + const cancellable = this._cancellable; + this._checkAliveTimeout = new PromiseUtils.TimeoutSecondsPromise(10, + GLib.PRIORITY_DEFAULT_IDLE, cancellable); + Util.Logger.debug(`${this.uniqueId}: may not respond, checking...`); + await this._checkAliveTimeout; + + // We should call the Ping method instead but in some containers + // such as snaps that's not accessible, so let's just use our own + await this._proxy.getProperty('Status', cancellable); + } catch (e) { + if (e.matches(Gio.DBusError, Gio.DBusError.NAME_HAS_NO_OWNER) || + e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN) || + e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_OBJECT) || + e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_INTERFACE) || + e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) || + e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_PROPERTY)) { + Util.Logger.warn(`${this.uniqueId}: not on bus anymore, removing it`); + this.destroy(); + return; + } + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + delete this._checkAliveTimeout; + } + } + + _onPropertiesChanged(_proxy, changed, _invalidated) { + const props = Object.keys(changed.unpack()); + const signalsToEmit = new Set(); + const checkIfReadyChanged = () => { + if (checkIfReadyChanged.value === undefined) + checkIfReadyChanged.value = this._checkIfReady(); + return checkIfReadyChanged.value; + }; + + props.forEach(property => { + // some property changes require updates on our part, + // a few need to be passed down to the displaying code + if (property === 'Id') + checkIfReadyChanged(); + + // all these can mean that the icon has to be changed + if (property.startsWith('Icon') || + property.startsWith('AttentionIcon')) + signalsToEmit.add('icon'); + + // same for overlays + if (property.startsWith('OverlayIcon')) + signalsToEmit.add('overlay-icon'); + + // this may make all of our icons invalid + if (property === 'IconThemePath') { + signalsToEmit.add('icon'); + signalsToEmit.add('overlay-icon'); + } + + // the label will be handled elsewhere + if (property === 'XAyatanaLabel') + signalsToEmit.add('label'); + + if (property === 'Menu') { + if (!checkIfReadyChanged() && this.isReady) + signalsToEmit.add('menu'); + } + + if (property === 'IconAccessibleDesc' || + property === 'AttentionAccessibleDesc' || + property === 'Title') + signalsToEmit.add('accessible-name'); + + // status updates may cause the indicator to be hidden + if (property === 'Status') { + signalsToEmit.add('icon'); + signalsToEmit.add('overlay-icon'); + signalsToEmit.add('status'); + signalsToEmit.add('accessible-name'); + } + }); + + signalsToEmit.forEach(s => this.emit(s)); + } + + reset() { + this.emit('reset'); + } + + destroy() { + this.emit('destroy'); + + this.disconnectAll(); + this._proxy.destroy(); + this._cancellable.cancel(); + this._invalidatedPixmapsIcons.clear(); + + if (this._nameWatcher) + this._nameWatcher.destroy(); + delete this._cancellable; + delete this._proxy; + delete this._nameWatcher; + } + + _getPixmapProperty(iconType) { + const propertyName = SNIconType.toPropertyName(iconType, + {isPixbuf: true}); + const pixmap = this._proxy.get_cached_property(propertyName); + const wasInvalidated = this._invalidatedPixmapsIcons.delete(iconType); + + if (!pixmap && wasInvalidated) { + this._proxy.refreshProperty(propertyName, { + skipEqualityCheck: true, + }).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + } + + return pixmap; + } + + invalidatePixmapProperty(iconType) { + this._invalidatedPixmapsIcons.add(iconType); + this._proxy.set_cached_property( + SNIconType.toPropertyName(iconType, {isPixbuf: true}), null); + } + + _getActivationToken(timestamp) { + const launchContext = global.create_app_launch_context(timestamp, -1); + const fakeAppInfo = Gio.AppInfo.create_from_commandline( + this._commandLine || 'true', this.id, + Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION); + return [launchContext, launchContext.get_startup_notify_id(fakeAppInfo, [])]; + } + + async provideActivationToken(timestamp) { + if (this._hasProvideXdgActivationToken === false) + return; + + const [launchContext, activationToken] = this._getActivationToken(timestamp); + try { + await this._proxy.ProvideXdgActivationTokenAsync(activationToken, + this._cancellable); + this._hasProvideXdgActivationToken = true; + } catch (e) { + launchContext.launch_failed(activationToken); + + if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) + this._hasProvideXdgActivationToken = false; + else + Util.Logger.warn(`${this.id}, failed to provide activation token: ${e.message}`); + } + } + + async open(x, y, timestamp) { + const cancellable = this._cancellable; + // we can't use WindowID because we're not able to get the x11 window id from a MetaWindow + // nor can we call any X11 functions. Luckily, the Activate method usually works fine. + // parameters are "an hint to the item where to show eventual windows" [sic] + // ... and don't seem to have any effect. + + try { + await this.provideActivationToken(timestamp); + await this._proxy.ActivateAsync(x, y, cancellable); + this.supportsActivation = true; + } catch (e) { + if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) { + this.supportsActivation = false; + Util.Logger.warn(`${this.id}, does not support activation: ${e.message}`); + return; + } + + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.critical(`${this.id}, failed to activate: ${e.message}`); + } + } + + async secondaryActivate(timestamp, x, y) { + const cancellable = this._cancellable; + + try { + await this.provideActivationToken(timestamp); + + if (this._hasAyatanaSecondaryActivate !== false) { + try { + await this._proxy.XAyatanaSecondaryActivateAsync(timestamp, cancellable); + this._hasAyatanaSecondaryActivate = true; + } catch (e) { + if (e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD)) + this._hasAyatanaSecondaryActivate = false; + else + throw e; + } + } + + if (!this._hasAyatanaSecondaryActivate) + await this._proxy.SecondaryActivateAsync(x, y, cancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.critical(`${this.id}, failed to secondary activate: ${e.message}`); + } + } + + async scroll(dx, dy) { + const cancellable = this._cancellable; + + try { + const actions = []; + + if (dx !== 0) { + actions.push(this._proxy.ScrollAsync(Math.floor(dx), + 'horizontal', cancellable)); + } + + if (dy !== 0) { + actions.push(this._proxy.ScrollAsync(Math.floor(dy), + 'vertical', cancellable)); + } + + await Promise.all(actions); + } catch (e) { + Util.Logger.critical(`${this.id}, failed to scroll: ${e.message}`); + } + } +} + +const StTextureCacheSkippingFileIcon = GObject.registerClass({ + Implements: [Gio.Icon], +}, class StTextureCacheSkippingFileIconImpl extends Gio.EmblemedIcon { + _init(params) { + // FIXME: We can't just inherit from Gio.FileIcon for some reason + super._init({gicon: new Gio.FileIcon(params)}); + } + + vfunc_to_tokens() { + // Disables the to_tokens() vfunc so that the icon to_string() + // method won't work and thus can't be kept forever around by + // StTextureCache, see the awesome debugging session in this thread: + // https://twitter.com/mild_sunrise/status/1458739604098621443 + // upstream bug is at: + // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/4944 + return [false, [], 0]; + } +}); + +export const IconActor = GObject.registerClass( +class AppIndicatorsIconActor extends St.Icon { + static get DEFAULT_STYLE() { + return 'padding: 0'; + } + + static get USER_WRITABLE_PATHS() { + if (!this._userWritablePaths) { + this._userWritablePaths = [ + GLib.get_user_cache_dir(), + GLib.get_user_data_dir(), + GLib.get_user_config_dir(), + GLib.get_user_runtime_dir(), + GLib.get_home_dir(), + GLib.get_tmp_dir(), + ]; + + this._userWritablePaths.push(Object.values(GLib.UserDirectory).slice( + 0, -1).map(dirId => GLib.get_user_special_dir(dirId))); + } + + return this._userWritablePaths; + } + + _init(indicator, iconSize) { + super._init({ + reactive: true, + style_class: 'system-status-icon', + fallbackIconName: FALLBACK_ICON_NAME, + }); + + this.name = this.constructor.name; + this.add_style_class_name('appindicator-icon'); + this.add_style_class_name('status-notifier-icon'); + this.set_style(AppIndicatorsIconActor.DEFAULT_STYLE); + + const themeContext = St.ThemeContext.get_for_stage(global.stage); + this.height = iconSize * themeContext.scale_factor; + + this._indicator = indicator; + this._customIcons = new Map(); + this._iconSize = iconSize; + this._iconCache = new IconCache.IconCache(); + this._cancellable = new Gio.Cancellable(); + this._loadingIcons = Object.create(null); + + Object.values(SNIconType).forEach(t => (this._loadingIcons[t] = new Map())); + + Util.connectSmart(this._indicator, 'icon', this, () => { + if (this.is_mapped()) + this._updateIcon(); + }); + Util.connectSmart(this._indicator, 'overlay-icon', this, () => { + if (this.is_mapped()) + this._updateIcon(); + }); + Util.connectSmart(this._indicator, 'reset', this, + () => this._invalidateIconWhenFullyReady()); + + const settings = SettingsManager.getDefaultGSettings(); + Util.connectSmart(settings, 'changed::icon-size', this, () => + this._updateWhenFullyReady()); + Util.connectSmart(settings, 'changed::custom-icons', this, () => { + this._updateCustomIcons(); + this._invalidateIconWhenFullyReady(); + }); + + if (GObject.signal_lookup('resource-scale-changed', this)) + this.connect('resource-scale-changed', () => this._invalidateIcon()); + else + this.connect('notify::resource-scale', () => this._invalidateIcon()); + + Util.connectSmart(themeContext, 'notify::scale-factor', this, tc => { + this._updateIconSize(); + this.height = this._iconSize * tc.scale_factor; + this.width = -1; + this._invalidateIcon(); + }); + + Util.connectSmart(Util.getDefaultTheme(), 'changed', this, + () => this._invalidateIconWhenFullyReady()); + + this.connect('notify::mapped', () => { + if (!this.is_mapped()) + this._updateWhenFullyReady(); + }); + + this._updateWhenFullyReady(); + + this.connect('destroy', () => { + this._iconCache.destroy(); + this._cancellable.cancel(); + this._cancellable = null; + this._indicator = null; + this._loadingIcons = null; + this._iconTheme = null; + }); + } + + get debugId() { + return this._indicator ? this._indicator.id : this.toString(); + } + + async _waitForFullyReady() { + const waitConditions = []; + + if (!this.is_mapped()) { + waitConditions.push(new PromiseUtils.SignalConnectionPromise( + this, 'notify::mapped', this._cancellable)); + } + + if (!this._indicator.isReady) { + waitConditions.push(new PromiseUtils.SignalConnectionPromise( + this._indicator, 'ready', this._cancellable)); + } + + if (!waitConditions.length) + return true; + + await Promise.all(waitConditions); + return this._waitForFullyReady(); + } + + async _updateWhenFullyReady() { + if (this._waitingReady) + return; + + try { + this._waitingReady = true; + await this._waitForFullyReady(); + + this._updateIconSize(); + this._updateIconClass(); + this._updateCustomIcons(); + this._invalidateIcon(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + delete this._waitingReady; + } + } + + _updateIconClass() { + if (!this._indicator) + return; + + this.add_style_class_name( + `appindicator-icon-${this._indicator.id.toLowerCase().replace(/_|\s/g, '-')}`); + } + + _cancelLoadingByType(iconType) { + this._loadingIcons[iconType].forEach(c => c.cancel()); + this._loadingIcons[iconType].clear(); + } + + _ensureNoIconIsLoading(iconType, id) { + if (this._loadingIcons[iconType].has(id)) { + Util.Logger.debug(`${this.debugId}, Icon ${id} Is still loading, ignoring the request`); + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, + 'Already in progress'); + } else if (this._loadingIcons[iconType].size > 0) { + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS, + 'Another icon is already loading'); + } + } + + _getIconLoadingCancellable(iconType, loadingId) { + try { + this._ensureNoIconIsLoading(iconType, loadingId); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) + throw e; + this._cancelLoadingByType(iconType); + } + + const cancellable = new Util.CancellableChild(this._cancellable); + this._loadingIcons[iconType].set(loadingId, cancellable); + + return cancellable; + } + + _cleanupIconLoadingCancellable(iconType, loadingId) { + if (this._loadingIcons) + this._loadingIcons[iconType].delete(loadingId); + } + + _getResourceScale() { + // Remove this when we remove support for versions earlier than 3.38 + const resourceScale = this.get_resource_scale(); + if (Array.isArray(resourceScale)) + return resourceScale[0] ? resourceScale[1] : 1.0; + + return resourceScale; + } + + // Will look the icon up in the cache, if it's found + // it will return it. Otherwise, it will create it and cache it. + async _cacheOrCreateIconByName(iconType, iconSize, iconScaling, iconName, themePath) { + const id = `${iconType}:${iconName}@${iconSize * iconScaling}:${themePath || ''}`; + let gicon = this._iconCache.get(id); + + if (gicon) + return gicon; + + const iconData = this._getIconData(iconName, themePath, iconSize, iconScaling); + const loadingId = iconData.file ? iconData.file.get_path() : id; + + const cancellable = await this._getIconLoadingCancellable(iconType, id); + try { + gicon = await this._createIconByIconData(iconData, iconSize, + iconScaling, cancellable); + } finally { + this._cleanupIconLoadingCancellable(iconType, loadingId); + } + if (gicon) + gicon = this._iconCache.add(id, gicon); + return gicon; + } + + _getIconLookupFlags(themeNode) { + let lookupFlags = 0; + + if (!themeNode) + return lookupFlags; + + const lookupFlagsEnum = St.IconLookupFlags; + const iconStyle = themeNode.get_icon_style(); + if (iconStyle === St.IconStyle.REGULAR) + lookupFlags |= lookupFlagsEnum.FORCE_REGULAR; + else if (iconStyle === St.IconStyle.SYMBOLIC) + lookupFlags |= lookupFlagsEnum.FORCE_SYMBOLIC; + + if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) + lookupFlags |= lookupFlagsEnum.DIR_RTL; + else + lookupFlags |= lookupFlagsEnum.DIR_LTR; + + return lookupFlags; + } + + async _createIconByIconData(iconData, iconSize, iconScaling, cancellable) { + const {file, name} = iconData; + + if (!file && !name) { + if (this._createIconIdle) { + throw new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING, + 'Already in progress'); + } + + try { + this._createIconIdle = new PromiseUtils.IdlePromise(GLib.PRIORITY_DEFAULT_IDLE, + cancellable); + await this._createIconIdle; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + throw e; + } finally { + delete this._createIconIdle; + } + return this.gicon; + } else if (this._createIconIdle) { + this._createIconIdle.cancel(); + delete this._createIconIdle; + } + + if (name) + return new Gio.ThemedIcon({name}); + + if (!file) + throw new Error('Neither file or name are set'); + + if (!this._isFileInWritableArea(file)) + return new Gio.FileIcon({file}); + + try { + const [format, width, height] = await GdkPixbuf.Pixbuf.get_file_info_async( + file.get_path(), cancellable); + + if (!format) { + Util.Logger.critical(`${this.debugId}, Invalid image format: ${file.get_path()}`); + return null; + } + + if (width >= height * 1.5) { + /* Hello indicator-multiload! */ + await this._loadCustomImage(file, + width, height, iconSize, iconScaling, cancellable); + return null; + } else { + /* We'll wrap the icon so that it won't be cached forever by the shell */ + return new StTextureCacheSkippingFileIcon({file}); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + Util.Logger.warn( + `${this.debugId}, Impossible to read image info from ` + + `path '${file ? file.get_path() : null}' or name '${name}': ${e}`); + } + throw e; + } + } + + async _loadCustomImage(file, width, height, iconSize, iconScaling, cancellable) { + const textureCache = St.TextureCache.get_default(); + const customImage = textureCache.load_file_async(file, -1, + height, 1, iconScaling); + + const setCustomImageActor = imageActor => { + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + const {content} = imageActor; + imageActor.content = null; + imageActor.destroy(); + + this._setImageContent(content, + width * scaleFactor, height * scaleFactor); + }; + + if (customImage.content) { + setCustomImageActor(customImage); + return; + } + + const imageContentPromise = new PromiseUtils.SignalConnectionPromise( + customImage, 'notify::content', cancellable); + const waitPromise = new PromiseUtils.TimeoutSecondsPromise( + 1, GLib.PRIORITY_DEFAULT, cancellable); + + const racingPromises = [imageContentPromise, waitPromise]; + + try { + await Promise.race(racingPromises); + if (!waitPromise.resolved()) + setCustomImageActor(customImage); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; + } finally { + racingPromises.forEach(p => p.cancel()); + } + } + + _isFileInWritableArea(file) { + // No need to use IO here, we can just do some assumptions + // print('Writable paths', IconActor.USER_WRITABLE_PATHS) + const path = file.get_path(); + return IconActor.USER_WRITABLE_PATHS.some(writablePath => + path.startsWith(writablePath)); + } + + _createIconTheme(searchPath = []) { + const iconTheme = new St.IconTheme(); + iconTheme.set_search_path(searchPath); + + return iconTheme; + } + + _getIconData(name, themePath, size, scale) { + const emptyIconData = {iconInfo: null, file: null, name: null}; + + if (!name) { + delete this._iconTheme; + return emptyIconData; + } + + // HACK: icon is a path name. This is not specified by the API, + // but at least indicator-sensors uses it. + if (name[0] === '/') { + delete this._iconTheme; + + const file = Gio.File.new_for_path(name); + return {file, iconInfo: null, name: null}; + } + + if (name.includes('.')) { + const splits = name.split('.'); + + if (['svg', 'png'].includes(splits[splits.length - 1])) + name = splits.slice(0, -1).join(''); + } + + if (themePath && Util.getDefaultTheme().get_search_path().includes(themePath)) + themePath = null; + + if (themePath) { + // If a theme path is provided, we need to lookup the icon ourself + // as St won't be able to do it unless we mess with default theme + // that is something we prefer not to do, as it would imply lots of + // St.TextureCache cleanups. + + const newSearchPath = [themePath]; + if (!this._iconTheme) { + this._iconTheme = this._createIconTheme(newSearchPath); + } else { + const currentSearchPath = this._iconTheme.get_search_path(); + + if (!currentSearchPath.includes(newSearchPath)) + this._iconTheme.set_search_path(newSearchPath); + } + + // try to look up the icon in the icon theme + const iconInfo = this._iconTheme.lookup_icon_for_scale(`${name}`, + size, scale, this._getIconLookupFlags(this.get_theme_node()) | + St.IconLookupFlags.GENERIC_FALLBACK); + + if (iconInfo) { + return { + iconInfo, + file: Gio.File.new_for_path(iconInfo.get_filename()), + name: null, + }; + } + + const logger = this.gicon ? Util.Logger.debug : Util.Logger.warn; + logger(`${this.debugId}, Impossible to lookup icon ` + + `for '${name}' in ${themePath}`); + + return emptyIconData; + } + + delete this._iconTheme; + return {name, iconInfo: null, file: null}; + } + + _setImageContent(content, width, height) { + this.set({ + content, + width, + height, + contentGravity: Clutter.ContentGravity.RESIZE_ASPECT, + fallbackIconName: null, + }); + } + + async _createIconFromPixmap(iconType, iconSize, iconScaling, scaleFactor, pixmapsVariant) { + const {pixmapVariant, width, height, rowStride} = + PixmapsUtils.getBestPixmap(pixmapsVariant, iconSize * iconScaling); + + const id = `__PIXMAP_ICON_${width}x${height}`; + + const imageContent = new St.ImageContent({ + preferredWidth: width, + preferredHeight: height, + }); + + imageContent.set_bytes(pixmapVariant.get_data_as_bytes(), PIXMAPS_FORMAT, + width, height, rowStride); + + if (iconType !== SNIconType.OVERLAY && !this._indicator.hasOverlayIcon) { + const scaledSize = iconSize * scaleFactor; + this._setImageContent(imageContent, scaledSize, scaledSize); + return null; + } + + const cancellable = this._getIconLoadingCancellable(iconType, id); + try { + // FIXME: async API results in a gray icon for some reason + const [inputStream] = imageContent.load(iconSize, cancellable); + return await GdkPixbuf.Pixbuf.new_from_stream_at_scale_async( + inputStream, -1, iconSize * iconScaling, true, cancellable); + } catch (e) { + // the image data was probably bogus. We don't really know why, but it _does_ happen. + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`${this.debugId}, Impossible to create image from data: ${e}`); + throw e; + } finally { + this._cleanupIconLoadingCancellable(iconType, id); + } + } + + // The icon cache Active flag will be set to true if the used gicon matches + // the cached one (as in some cases it may be equal, but not the same object). + // So when it's not need anymore we make sure to check the active state + // and set it to false so that it can be picked up by the garbage collector. + _setGicon(iconType, gicon) { + if (iconType !== SNIconType.OVERLAY) { + if (gicon) { + if (this.gicon === gicon || + (this.gicon && this.gicon.get_icon() === gicon)) + return; + + if (gicon instanceof Gio.EmblemedIcon) + this.gicon = gicon; + else + this.gicon = new Gio.EmblemedIcon({gicon}); + + this._iconCache.updateActive(SNIconType.NORMAL, gicon, + this.gicon.get_icon() === gicon); + } else { + this.gicon = null; + } + } else if (gicon) { + this._emblem = new Gio.Emblem({icon: gicon}); + this._iconCache.updateActive(iconType, gicon, true); + } else { + this._emblem = null; + } + + if (this.gicon) { + if (!this.gicon.get_emblems().some(e => e.equal(this._emblem))) { + this.gicon.clear_emblems(); + if (this._emblem) + this.gicon.add_emblem(this._emblem); + } + } + } + + async _updateIconByType(iconType, iconSize) { + let icon; + switch (iconType) { + case SNIconType.ATTENTION: + icon = this._indicator.attentionIcon; + break; + case SNIconType.NORMAL: + ({icon} = this._indicator); + break; + case SNIconType.OVERLAY: + icon = this._indicator.overlayIcon; + break; + } + + const {theme, name, pixmap} = icon; + const commonArgs = [theme, iconType, iconSize]; + + if (this._customIcons.size) { + let customIcon = this._customIcons.get(iconType); + if (!await this._createAndSetIcon(customIcon, null, ...commonArgs)) { + if (iconType !== SNIconType.OVERLAY) { + customIcon = this._customIcons.get(SNIconType.NORMAL); + await this._createAndSetIcon(customIcon, null, ...commonArgs); + } + } + } else { + await this._createAndSetIcon(name, pixmap, ...commonArgs); + } + } + + async _createAndSetIcon(name, pixmap, theme, iconType, iconSize) { + let gicon = null; + + try { + gicon = await this._createIcon(name, pixmap, theme, iconType, iconSize); + } catch (e) { + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) || + e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) + return null; + + if (iconType === SNIconType.OVERLAY) { + logError(e, `${this.debugId} unable to update icon emblem`); + } else { + this.fallbackIconName = FALLBACK_ICON_NAME; + logError(e, `${this.debugId} unable to update icon`); + } + } + + try { + this._setGicon(iconType, gicon); + + if (pixmap && this.gicon) { + // The pixmap has been saved, we can free the variants memory + this._indicator.invalidatePixmapProperty(iconType); + } + + return gicon; + } catch (e) { + logError(e, 'Setting GIcon failed'); + return null; + } + } + + // updates the base icon + async _createIcon(name, pixmap, theme, iconType, iconSize) { + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + const resourceScale = this._getResourceScale(); + const iconScaling = Math.ceil(resourceScale * scaleFactor); + + // From now on we consider them the same thing, as one replaces the other + if (iconType === SNIconType.ATTENTION) + iconType = SNIconType.NORMAL; + + if (name) { + const gicon = await this._cacheOrCreateIconByName( + iconType, iconSize, iconScaling, name, theme); + if (gicon) + return gicon; + } + + if (pixmap && pixmap.n_children()) { + return this._createIconFromPixmap(iconType, + iconSize, iconScaling, scaleFactor, pixmap); + } + + return null; + } + + // updates the base icon + async _updateIcon() { + if (this._indicator.status === SNIStatus.PASSIVE) + return; + + if (this.gicon instanceof Gio.EmblemedIcon) { + const {gicon} = this.gicon; + this._iconCache.updateActive(SNIconType.NORMAL, gicon, false); + } + + // we might need to use the AttentionIcon*, which have precedence over the normal icons + const iconType = this._indicator.status === SNIStatus.NEEDS_ATTENTION + ? SNIconType.ATTENTION : SNIconType.NORMAL; + + try { + await this._updateIconByType(iconType, this._iconSize); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) && + !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) + logError(e, `${this.debugId}: Updating icon type ${iconType} failed`); + } + } + + async _updateOverlayIcon() { + if (this._indicator.status === SNIStatus.PASSIVE) + return; + + if (this._emblem) { + const {icon} = this._emblem; + this._iconCache.updateActive(SNIconType.OVERLAY, icon, false); + } + + // KDE hardcodes the overlay icon size to 10px (normal icon size 16px) + // we approximate that ratio for other sizes, too. + // our algorithms will always pick a smaller one instead of stretching it. + const iconSize = Math.floor(this._iconSize / 1.6); + + try { + await this._updateIconByType(SNIconType.OVERLAY, iconSize); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED) && + !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.PENDING)) + logError(e, `${this.debugId}: Updating overlay icon failed`); + } + } + + async _invalidateIconWhenFullyReady() { + if (this._waitingInvalidation) + return; + + try { + this._waitingInvalidation = true; + await this._waitForFullyReady(); + this._invalidateIcon(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + delete this._waitingInvalidation; + } + } + + // called when the icon theme changes + _invalidateIcon() { + this._iconCache.clear(); + this._cancellable.cancel(); + this._cancellable = new Gio.Cancellable(); + Object.values(SNIconType).forEach(iconType => + this._loadingIcons[iconType].clear()); + + this._updateIcon().catch(e => logError(e)); + this._updateOverlayIcon().catch(e => logError(e)); + } + + _updateIconSize() { + const settings = SettingsManager.getDefaultGSettings(); + const sizeValue = settings.get_int('icon-size'); + + if (sizeValue > 0) { + if (!this._defaultIconSize) + this._defaultIconSize = this._iconSize; + + this._iconSize = sizeValue; + } else if (this._defaultIconSize) { + this._iconSize = this._defaultIconSize; + delete this._defaultIconSize; + } + + const themeIconSize = Math.round( + this.get_theme_node().get_length('icon-size')); + let iconStyle = AppIndicatorsIconActor.DEFAULT_STYLE; + + if (themeIconSize > 0) { + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + + if (themeIconSize / scaleFactor !== this._iconSize) { + iconStyle = `${AppIndicatorsIconActor.DEFAULT_STYLE};` + + 'icon-size: 0'; + } + } + + this.set_style(iconStyle); + this.set_icon_size(this._iconSize); + } + + _updateCustomIcons() { + const settings = SettingsManager.getDefaultGSettings(); + this._customIcons.clear(); + + settings.get_value('custom-icons').deep_unpack().forEach(customIcons => { + const [indicatorId, normalIcon, attentionIcon] = customIcons; + if (this._indicator.id === indicatorId) { + this._customIcons.set(SNIconType.NORMAL, normalIcon); + this._customIcons.set(SNIconType.ATTENTION, attentionIcon); + } + }); + } +}); diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js new file mode 100644 index 0000000..4f9467f --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusMenu.js @@ -0,0 +1,950 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Clutter from 'gi://Clutter'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import GdkPixbuf from 'gi://GdkPixbuf'; +import Gio from 'gi://Gio'; +import St from 'gi://St'; + +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +import * as DBusInterfaces from './interfaces.js'; +import * as PromiseUtils from './promiseUtils.js'; +import * as Util from './util.js'; +import {DBusProxy} from './dbusProxy.js'; + +Gio._promisify(GdkPixbuf.Pixbuf, 'new_from_stream_async'); + +// //////////////////////////////////////////////////////////////////////// +// PART ONE: "ViewModel" backend implementation. +// Both code and design are inspired by libdbusmenu +// //////////////////////////////////////////////////////////////////////// + +/** + * Saves menu property values and handles type checking and defaults + */ +export class PropertyStore { + constructor(initialProperties) { + this._props = new Map(); + + if (initialProperties) { + for (const [prop, value] of Object.entries(initialProperties)) + this.set(prop, value); + } + } + + set(name, value) { + if (name in PropertyStore.MandatedTypes && value && + !value.is_of_type(PropertyStore.MandatedTypes[name])) + Util.Logger.warn(`Cannot set property ${name}: type mismatch!`); + else if (value) + this._props.set(name, value); + else + this._props.delete(name); + } + + get(name) { + const prop = this._props.get(name); + if (prop) + return prop; + else if (name in PropertyStore.DefaultValues) + return PropertyStore.DefaultValues[name]; + else + return null; + } +} + +// we list all the properties we know and use here, so we won' have to deal with unexpected type mismatches +PropertyStore.MandatedTypes = { + 'visible': GLib.VariantType.new('b'), + 'enabled': GLib.VariantType.new('b'), + 'label': GLib.VariantType.new('s'), + 'type': GLib.VariantType.new('s'), + 'children-display': GLib.VariantType.new('s'), + 'icon-name': GLib.VariantType.new('s'), + 'icon-data': GLib.VariantType.new('ay'), + 'toggle-type': GLib.VariantType.new('s'), + 'toggle-state': GLib.VariantType.new('i'), +}; + +PropertyStore.DefaultValues = { + 'visible': GLib.Variant.new_boolean(true), + 'enabled': GLib.Variant.new_boolean(true), + 'label': GLib.Variant.new_string(''), + 'type': GLib.Variant.new_string('standard'), + // elements not in here must return null +}; + +/** + * Represents a single menu item + */ +export class DbusMenuItem extends Signals.EventEmitter { + // will steal the properties object + constructor(client, id, properties, childrenIds) { + super(); + + this._client = client; + this._id = id; + this._propStore = new PropertyStore(properties); + this._children_ids = childrenIds; + } + + propertyGet(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_string()[0] : null; + } + + propertyGetVariant(propName) { + return this._propStore.get(propName); + } + + propertyGetBool(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_boolean() : false; + } + + propertyGetInt(propName) { + const prop = this.propertyGetVariant(propName); + return prop ? prop.get_int32() : 0; + } + + propertySet(prop, value) { + this._propStore.set(prop, value); + + this.emit('property-changed', prop, this.propertyGetVariant(prop)); + } + + getChildrenIds() { + return this._children_ids.concat(); // clone it! + } + + addChild(pos, childId) { + this._children_ids.splice(pos, 0, childId); + this.emit('child-added', this._client.getItem(childId), pos); + } + + removeChild(childId) { + // find it + let pos = -1; + for (let i = 0; i < this._children_ids.length; ++i) { + if (this._children_ids[i] === childId) { + pos = i; + break; + } + } + + if (pos < 0) { + Util.Logger.critical("Trying to remove child which doesn't exist"); + } else { + this._children_ids.splice(pos, 1); + this.emit('child-removed', this._client.getItem(childId)); + } + } + + moveChild(childId, newPos) { + // find the old position + let oldPos = -1; + for (let i = 0; i < this._children_ids.length; ++i) { + if (this._children_ids[i] === childId) { + oldPos = i; + break; + } + } + + if (oldPos < 0) { + Util.Logger.critical("tried to move child which wasn't in the list"); + return; + } + + if (oldPos !== newPos) { + this._children_ids.splice(oldPos, 1); + this._children_ids.splice(newPos, 0, childId); + this.emit('child-moved', oldPos, newPos, this._client.getItem(childId)); + } + } + + getChildren() { + return this._children_ids.map(el => this._client.getItem(el)); + } + + handleEvent(event, data, timestamp) { + if (!data) + data = GLib.Variant.new_int32(0); + + this._client.sendEvent(this._id, event, data, timestamp); + } + + getId() { + return this._id; + } + + sendAboutToShow() { + this._client.sendAboutToShow(this._id); + } +} + + +/** + * The client does the heavy lifting of actually reading layouts and distributing events + */ + +export const DBusClient = GObject.registerClass({ + Signals: {'ready-changed': {}}, +}, class AppIndicatorsDBusClient extends DBusProxy { + static get interfaceInfo() { + if (!this._interfaceInfo) { + this._interfaceInfo = Gio.DBusInterfaceInfo.new_for_xml( + DBusInterfaces.DBusMenu); + } + return this._interfaceInfo; + } + + static get baseItems() { + if (!this._baseItems) { + this._baseItems = { + 'children-display': GLib.Variant.new_string('submenu'), + }; + } + return this._baseItems; + } + + static destroy() { + delete this._interfaceInfo; + } + + _init(busName, objectPath) { + const {interfaceInfo} = AppIndicatorsDBusClient; + + super._init(busName, objectPath, interfaceInfo, + Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES); + + this._items = new Map(); + this._items.set(0, new DbusMenuItem(this, 0, DBusClient.baseItems, [])); + this._flagItemsUpdateRequired = false; + + // will be set to true if a layout update is needed once active + this._flagLayoutUpdateRequired = false; + + // property requests are queued + this._propertiesRequestedFor = new Set(/* ids */); + + this._layoutUpdated = false; + this._active = false; + } + + async initAsync(cancellable) { + await super.initAsync(cancellable); + + this._requestLayoutUpdate(); + } + + _onNameOwnerChanged() { + if (this.isReady) + this._requestLayoutUpdate(); + } + + get isReady() { + return this._layoutUpdated && !!this.gNameOwner; + } + + get cancellable() { + return this._cancellable; + } + + getRoot() { + return this._items.get(0); + } + + _requestLayoutUpdate() { + const cancellable = new Util.CancellableChild(this._cancellable); + this._beginLayoutUpdate(cancellable); + } + + async _requestProperties(propertyId, cancellable) { + this._propertiesRequestedFor.add(propertyId); + + if (this._propertiesRequest && this._propertiesRequest.pending()) + return; + + // if we don't have any requests queued, we'll need to add one + this._propertiesRequest = new PromiseUtils.IdlePromise( + GLib.PRIORITY_DEFAULT_IDLE, cancellable); + await this._propertiesRequest; + + const requestedProperties = Array.from(this._propertiesRequestedFor); + this._propertiesRequestedFor.clear(); + const [result] = await this.GetGroupPropertiesAsync(requestedProperties, + [], cancellable); + + result.forEach(([id, properties]) => { + const item = this._items.get(id); + if (!item) + return; + + for (const [prop, value] of Object.entries(properties)) + item.propertySet(prop, value); + }); + } + + // Traverses the list of cached menu items and removes everyone that is not in the list + // so we don't keep alive unused items + _gcItems() { + const tag = new Date().getTime(); + + const toTraverse = [0]; + while (toTraverse.length > 0) { + const item = this.getItem(toTraverse.shift()); + item._dbusClientGcTag = tag; + Array.prototype.push.apply(toTraverse, item.getChildrenIds()); + } + + this._items.forEach((i, id) => { + if (i._dbusClientGcTag !== tag) + this._items.delete(id); + }); + } + + // the original implementation will only request partial layouts if somehow possible + // we try to save us from multiple kinds of race conditions by always requesting a full layout + _beginLayoutUpdate(cancellable) { + this._layoutUpdateUpdateAsync(cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + } + + // the original implementation will only request partial layouts if somehow possible + // we try to save us from multiple kinds of race conditions by always requesting a full layout + async _layoutUpdateUpdateAsync(cancellable) { + // we only read the type property, because if the type changes after reading all properties, + // the view would have to replace the item completely which we try to avoid + if (this._layoutUpdateCancellable) + this._layoutUpdateCancellable.cancel(); + + this._layoutUpdateCancellable = cancellable; + + try { + const [revision_, root] = await this.GetLayoutAsync(0, -1, + ['type', 'children-display'], cancellable); + + this._updateLayoutState(true); + this._doLayoutUpdate(root, cancellable); + this._gcItems(); + this._flagLayoutUpdateRequired = false; + this._flagItemsUpdateRequired = false; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + this._updateLayoutState(false); + throw e; + } finally { + if (this._layoutUpdateCancellable === cancellable) + this._layoutUpdateCancellable = null; + } + } + + _updateLayoutState(state) { + const wasReady = this.isReady; + this._layoutUpdated = state; + if (this.isReady !== wasReady) + this.emit('ready-changed'); + } + + _doLayoutUpdate(item, cancellable) { + const [id, properties, children] = item; + + const childrenUnpacked = children.map(c => c.deep_unpack()); + const childrenIds = childrenUnpacked.map(([c]) => c); + + // make sure all our children exist + childrenUnpacked.forEach(c => this._doLayoutUpdate(c, cancellable)); + + // make sure we exist + const menuItem = this._items.get(id); + + if (menuItem) { + // we do, update our properties if necessary + for (const [prop, value] of Object.entries(properties)) + menuItem.propertySet(prop, value); + + // make sure our children are all at the right place, and exist + const oldChildrenIds = menuItem.getChildrenIds(); + for (let i = 0; i < childrenIds.length; ++i) { + // try to recycle an old child + let oldChild = -1; + for (let j = 0; j < oldChildrenIds.length; ++j) { + if (oldChildrenIds[j] === childrenIds[i]) { + [oldChild] = oldChildrenIds.splice(j, 1); + break; + } + } + + if (oldChild < 0) { + // no old child found, so create a new one! + menuItem.addChild(i, childrenIds[i]); + } else { + // old child found, reuse it! + menuItem.moveChild(childrenIds[i], i); + } + } + + // remove any old children that weren't reused + oldChildrenIds.forEach(c => menuItem.removeChild(c)); + + if (!this._flagItemsUpdateRequired) + return id; + } + + // we don't, so let's create us + let newMenuItem = menuItem; + + if (!newMenuItem) { + newMenuItem = new DbusMenuItem(this, id, properties, childrenIds); + this._items.set(id, newMenuItem); + } + + this._requestProperties(id, cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); + }); + + return id; + } + + async _doPropertiesUpdateAsync(cancellable) { + if (this._propertiesUpdateCancellable) + this._propertiesUpdateCancellable.cancel(); + + this._propertiesUpdateCancellable = cancellable; + + try { + const requests = []; + + this._items.forEach((_, id) => + requests.push(this._requestProperties(id, cancellable))); + + await Promise.all(requests); + } finally { + if (this._propertiesUpdateCancellable === cancellable) + this._propertiesUpdateCancellable = null; + } + } + + _doPropertiesUpdate() { + const cancellable = new Util.CancellableChild(this._cancellable); + this._doPropertiesUpdateAsync(cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + Util.Logger.warn(`Could not get menu properties menu proxy: ${e}`); + }); + } + + + set active(active) { + const wasActive = this._active; + this._active = active; + + if (active && wasActive !== active) { + if (this._flagLayoutUpdateRequired) { + this._requestLayoutUpdate(); + } else if (this._flagItemsUpdateRequired) { + this._doPropertiesUpdate(); + this._flagItemsUpdateRequired = false; + } + } + } + + _onSignal(_sender, signal, params) { + if (signal === 'LayoutUpdated') { + if (!this._active) { + this._flagLayoutUpdateRequired = true; + return; + } + + this._requestLayoutUpdate(); + } else if (signal === 'ItemsPropertiesUpdated') { + if (!this._active) { + this._flagItemsUpdateRequired = true; + return; + } + + this._onPropertiesUpdated(params.deep_unpack()); + } + } + + getItem(id) { + const item = this._items.get(id); + if (!item) + Util.Logger.warn(`trying to retrieve item for non-existing id ${id} !?`); + return item || null; + } + + // we don't need to cache and burst-send that since it will not happen that frequently + async sendAboutToShow(id) { + /* Some indicators (you, dropbox!) don't use the right signature + * and don't return a boolean, so we need to support both cases */ + try { + const ret = await this.gConnection.call(this.gName, this.gObjectPath, + this.gInterfaceName, 'AboutToShow', new GLib.Variant('(i)', [id]), + null, Gio.DBusCallFlags.NONE, -1, this._cancellable); + + if ((ret.is_of_type(new GLib.VariantType('(b)')) && + ret.get_child_value(0).get_boolean()) || + ret.is_of_type(new GLib.VariantType('()'))) + this._requestLayoutUpdate(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + + sendEvent(id, event, params, timestamp) { + if (!this.gNameOwner) + return; + + this.EventAsync(id, event, params, timestamp, this._cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + } + + _onPropertiesUpdated([changed, removed]) { + changed.forEach(([id, props]) => { + const item = this._items.get(id); + if (!item) + return; + + for (const [prop, value] of Object.entries(props)) + item.propertySet(prop, value); + }); + removed.forEach(([id, propNames]) => { + const item = this._items.get(id); + if (!item) + return; + + propNames.forEach(propName => item.propertySet(propName, null)); + }); + } +}); + +// //////////////////////////////////////////////////////////////////////// +// PART TWO: "View" frontend implementation. +// //////////////////////////////////////////////////////////////////////// + +// https://bugzilla.gnome.org/show_bug.cgi?id=731514 +// GNOME 3.10 and 3.12 can't open a nested submenu. +// Patches have been written, but it's not clear when (if?) they will be applied. +// We also don't know whether they will be backported to 3.10, so we will work around +// it in the meantime. Offending versions can be clearly identified: +const NEED_NESTED_SUBMENU_FIX = '_setOpenedSubMenu' in PopupMenu.PopupMenu.prototype; + +/** + * Creates new wrapper menu items and injects methods for managing them at runtime. + * + * Many functions in this object will be bound to the created item and executed as event + * handlers, so any `this` will refer to a menu item create in createItem + */ +const MenuItemFactory = { + createItem(client, dbusItem) { + // first, decide whether it's a submenu or not + let shellItem; + if (dbusItem.propertyGet('children-display') === 'submenu') + shellItem = new PopupMenu.PopupSubMenuMenuItem('FIXME'); + else if (dbusItem.propertyGet('type') === 'separator') + shellItem = new PopupMenu.PopupSeparatorMenuItem(''); + else + shellItem = new PopupMenu.PopupMenuItem('FIXME'); + + shellItem._dbusItem = dbusItem; + shellItem._dbusClient = client; + + if (shellItem instanceof PopupMenu.PopupMenuItem) { + shellItem._icon = new St.Icon({ + style_class: 'popup-menu-icon', + xAlign: Clutter.ActorAlign.END, + }); + shellItem.add_child(shellItem._icon); + shellItem.label.x_expand = true; + } + + // initialize our state + MenuItemFactory._updateLabel.call(shellItem); + MenuItemFactory._updateOrnament.call(shellItem); + MenuItemFactory._updateImage.call(shellItem); + MenuItemFactory._updateVisible.call(shellItem); + MenuItemFactory._updateSensitive.call(shellItem); + + // initially create children + if (shellItem instanceof PopupMenu.PopupSubMenuMenuItem) { + dbusItem.getChildren().forEach(c => + shellItem.menu.addMenuItem(MenuItemFactory.createItem(client, c))); + } + + // now, connect various events + Util.connectSmart(dbusItem, 'property-changed', + shellItem, MenuItemFactory._onPropertyChanged); + Util.connectSmart(dbusItem, 'child-added', + shellItem, MenuItemFactory._onChildAdded); + Util.connectSmart(dbusItem, 'child-removed', + shellItem, MenuItemFactory._onChildRemoved); + Util.connectSmart(dbusItem, 'child-moved', + shellItem, MenuItemFactory._onChildMoved); + Util.connectSmart(shellItem, 'activate', + shellItem, MenuItemFactory._onActivate); + + shellItem.connect('destroy', () => { + shellItem._dbusItem = null; + shellItem._dbusClient = null; + shellItem._icon = null; + }); + + if (shellItem.menu) { + Util.connectSmart(shellItem.menu, 'open-state-changed', + shellItem, MenuItemFactory._onOpenStateChanged); + } + + return shellItem; + }, + + _onOpenStateChanged(menu, open) { + if (open) { + if (NEED_NESTED_SUBMENU_FIX) { + // close our own submenus + if (menu._openedSubMenu) + menu._openedSubMenu.close(false); + + // register ourselves and close sibling submenus + if (menu._parent._openedSubMenu && menu._parent._openedSubMenu !== menu) + menu._parent._openedSubMenu.close(true); + + menu._parent._openedSubMenu = menu; + } + + this._dbusItem.handleEvent('opened', null, 0); + this._dbusItem.sendAboutToShow(); + } else { + if (NEED_NESTED_SUBMENU_FIX) { + // close our own submenus + if (menu._openedSubMenu) + menu._openedSubMenu.close(false); + } + + this._dbusItem.handleEvent('closed', null, 0); + } + }, + + _onActivate(_item, event) { + const timestamp = event.get_time(); + if (timestamp && this._dbusClient.indicator) + this._dbusClient.indicator.provideActivationToken(timestamp); + + this._dbusItem.handleEvent('clicked', GLib.Variant.new('i', 0), + timestamp); + }, + + _onPropertyChanged(dbusItem, prop, _value) { + if (prop === 'toggle-type' || prop === 'toggle-state') + MenuItemFactory._updateOrnament.call(this); + else if (prop === 'label') + MenuItemFactory._updateLabel.call(this); + else if (prop === 'enabled') + MenuItemFactory._updateSensitive.call(this); + else if (prop === 'visible') + MenuItemFactory._updateVisible.call(this); + else if (prop === 'icon-name' || prop === 'icon-data') + MenuItemFactory._updateImage.call(this); + else if (prop === 'type' || prop === 'children-display') + MenuItemFactory._replaceSelf.call(this); + else + Util.Logger.debug(`Unhandled property change: ${prop}`); + }, + + _onChildAdded(dbusItem, child, position) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to add a child to non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + this.menu.addMenuItem(MenuItemFactory.createItem(this._dbusClient, child), position); + } + }, + + _onChildRemoved(dbusItem, child) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to remove a child from non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + // find it! + this.menu._getMenuItems().forEach(item => { + if (item._dbusItem === child) + item.destroy(); + }); + } + }, + + _onChildMoved(dbusItem, child, oldpos, newpos) { + if (!(this instanceof PopupMenu.PopupSubMenuMenuItem)) { + Util.Logger.warn('Tried to move a child in non-submenu item. Better recreate it as whole'); + MenuItemFactory._replaceSelf.call(this); + } else { + MenuUtils.moveItemInMenu(this.menu, child, newpos); + } + }, + + _updateLabel() { + const label = this._dbusItem.propertyGet('label').replace(/_([^_])/, '$1'); + + if (this.label) // especially on GS3.8, the separator item might not even have a hidden label + this.label.set_text(label); + }, + + _updateOrnament() { + if (!this.setOrnament) + return; // separators and alike might not have gotten the polyfill + + if (this._dbusItem.propertyGet('toggle-type') === 'checkmark' && + this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.CHECK); + else if (this._dbusItem.propertyGet('toggle-type') === 'radio' && + this._dbusItem.propertyGetInt('toggle-state')) + this.setOrnament(PopupMenu.Ornament.DOT); + else + this.setOrnament(PopupMenu.Ornament.NONE); + }, + + async _updateImage() { + if (!this._icon) + return; // might be missing on submenus / separators + + const iconName = this._dbusItem.propertyGet('icon-name'); + const iconData = this._dbusItem.propertyGetVariant('icon-data'); + if (iconName) { + this._icon.icon_name = iconName; + } else if (iconData) { + try { + const inputStream = Gio.MemoryInputStream.new_from_bytes( + iconData.get_data_as_bytes()); + this._icon.gicon = await GdkPixbuf.Pixbuf.new_from_stream_async( + inputStream, this._dbusClient.cancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + }, + + _updateVisible() { + this.visible = this._dbusItem.propertyGetBool('visible'); + }, + + _updateSensitive() { + this.setSensitive(this._dbusItem.propertyGetBool('enabled')); + }, + + _replaceSelf(newSelf) { + // create our new self if needed + if (!newSelf) + newSelf = MenuItemFactory.createItem(this._dbusClient, this._dbusItem); + + // first, we need to find our old position + let pos = -1; + const family = this._parent._getMenuItems(); + for (let i = 0; i < family.length; ++i) { + if (family[i] === this) + pos = i; + } + + if (pos < 0) + throw new Error("DBusMenu: can't replace non existing menu item"); + + + // add our new self while we're still alive + this._parent.addMenuItem(newSelf, pos); + + // now destroy our old self + this.destroy(); + }, +}; + +/** + * Utility functions not necessarily belonging into the item factory + */ +const MenuUtils = { + moveItemInMenu(menu, dbusItem, newpos) { + // HACK: we're really getting into the internals of the PopupMenu implementation + + // First, find our wrapper. Children tend to lie. We do not trust the old positioning. + const family = menu._getMenuItems(); + for (let i = 0; i < family.length; ++i) { + if (family[i]._dbusItem === dbusItem) { + // now, remove it + menu.box.remove_child(family[i]); + + // and add it again somewhere else + if (newpos < family.length && family[newpos] !== family[i]) + menu.box.insert_child_below(family[i], family[newpos]); + else + menu.box.add(family[i]); + + // skip the rest + return; + } + } + }, +}; + + +/** + * Processes DBus events, creates the menu items and handles the actions + * + * Something like a mini-god-object + */ +export class Client extends Signals.EventEmitter { + constructor(busName, path, indicator) { + super(); + + this._busName = busName; + this._busPath = path; + this._client = new DBusClient(busName, path); + this._rootMenu = null; // the shell menu + this._rootItem = null; // the DbusMenuItem for the root + this.indicator = indicator; + this.cancellable = new Util.CancellableChild(this.indicator.cancellable); + + this._client.initAsync(this.cancellable).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }); + + Util.connectSmart(this._client, 'ready-changed', this, + () => this.emit('ready-changed')); + } + + get isReady() { + return this._client.isReady; + } + + // this will attach the client to an already existing menu that will be used as the root menu. + // it will also connect the client to be automatically destroyed when the menu dies. + attachToMenu(menu) { + this._rootMenu = menu; + this._rootItem = this._client.getRoot(); + this._itemsBeingAdded = new Set(); + + // cleanup: remove existing children (just in case) + this._rootMenu.removeAll(); + + if (NEED_NESTED_SUBMENU_FIX) + menu._setOpenedSubMenu = this._setOpenedSubmenu.bind(this); + + // connect handlers + Util.connectSmart(menu, 'open-state-changed', this, this._onMenuOpened); + Util.connectSmart(menu, 'destroy', this, this.destroy); + + Util.connectSmart(this._rootItem, 'child-added', this, this._onRootChildAdded); + Util.connectSmart(this._rootItem, 'child-removed', this, this._onRootChildRemoved); + Util.connectSmart(this._rootItem, 'child-moved', this, this._onRootChildMoved); + + // Dropbox requires us to call AboutToShow(0) first + this._rootItem.sendAboutToShow(); + + // fill the menu for the first time + const children = this._rootItem.getChildren(); + children.forEach(child => + this._onRootChildAdded(this._rootItem, child)); + } + + _setOpenedSubmenu(submenu) { + if (!submenu) + return; + + if (submenu._parent !== this._rootMenu) + return; + + if (submenu === this._openedSubMenu) + return; + + if (this._openedSubMenu && this._openedSubMenu.isOpen) + this._openedSubMenu.close(true); + + this._openedSubMenu = submenu; + } + + _onRootChildAdded(dbusItem, child, position) { + // Menu additions can be expensive, so let's do it in different chunks + const basePriority = this.isOpen ? GLib.PRIORITY_DEFAULT : GLib.PRIORITY_LOW; + const idlePromise = new PromiseUtils.IdlePromise( + basePriority + this._itemsBeingAdded.size, this.cancellable); + this._itemsBeingAdded.add(child); + + idlePromise.then(() => { + if (!this._itemsBeingAdded.has(child)) + return; + + this._rootMenu.addMenuItem( + MenuItemFactory.createItem(this, child), position); + }).catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + }).finally(() => this._itemsBeingAdded.delete(child)); + } + + _onRootChildRemoved(dbusItem, child) { + // children like to play hide and seek + // but we know how to find it for sure! + const item = this._rootMenu._getMenuItems().find(it => + it._dbusItem === child); + + if (item) + item.destroy(); + else + this._itemsBeingAdded.delete(child); + } + + _onRootChildMoved(dbusItem, child, oldpos, newpos) { + MenuUtils.moveItemInMenu(this._rootMenu, dbusItem, newpos); + } + + _onMenuOpened(menu, state) { + if (!this._rootItem) + return; + + this._client.active = state; + + if (state) { + if (this._openedSubMenu && this._openedSubMenu.isOpen) + this._openedSubMenu.close(); + + this._rootItem.handleEvent('opened', null, 0); + this._rootItem.sendAboutToShow(); + } else { + this._rootItem.handleEvent('closed', null, 0); + } + } + + destroy() { + this.emit('destroy'); + + if (this._client) + this._client.destroy(); + + this._client = null; + this._rootItem = null; + this._rootMenu = null; + this.indicator = null; + this._itemsBeingAdded = null; + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js new file mode 100644 index 0000000..edf16fc --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/dbusProxy.js @@ -0,0 +1,103 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + +import {CancellableChild, Logger} from './util.js'; + +Gio._promisify(Gio.DBusProxy.prototype, 'init_async'); + +export const DBusProxy = GObject.registerClass({ + Signals: {'destroy': {}}, +}, class DBusProxy extends Gio.DBusProxy { + static get TUPLE_VARIANT_TYPE() { + if (!this._tupleVariantType) + this._tupleVariantType = new GLib.VariantType('(v)'); + + return this._tupleVariantType; + } + + static destroy() { + delete this._tupleType; + } + + _init(busName, objectPath, interfaceInfo, flags = Gio.DBusProxyFlags.NONE) { + if (interfaceInfo.signals.length) + Logger.warn('Avoid exposing signals to gjs!'); + + super._init({ + gConnection: Gio.DBus.session, + gInterfaceName: interfaceInfo.name, + gInterfaceInfo: interfaceInfo, + gName: busName, + gObjectPath: objectPath, + gFlags: flags, + }); + + this._signalIds = []; + + if (!(flags & Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS)) { + this._signalIds.push(this.connect('g-signal', + (_proxy, ...args) => this._onSignal(...args))); + } + + this._signalIds.push(this.connect('notify::g-name-owner', () => + this._onNameOwnerChanged())); + } + + async initAsync(cancellable) { + cancellable = new CancellableChild(cancellable); + await this.init_async(GLib.PRIORITY_DEFAULT, cancellable); + this._cancellable = cancellable; + + this.gInterfaceInfo.methods.map(m => m.name).forEach(method => + this._ensureAsyncMethod(method)); + } + + destroy() { + this.emit('destroy'); + + this._signalIds.forEach(id => this.disconnect(id)); + + if (this._cancellable) + this._cancellable.cancel(); + } + + // This can be removed when we will have GNOME 43 as minimum version + _ensureAsyncMethod(method) { + if (this[`${method}Async`]) + return; + + if (!this[`${method}Remote`]) + throw new Error(`Missing remote method '${method}'`); + + this[`${method}Async`] = function (...args) { + return new Promise((resolve, reject) => { + this[`${method}Remote`](...args, (ret, e) => { + if (e) + reject(e); + else + resolve(ret); + }); + }); + }; + } + + _onSignal() { + } + + getProperty(propertyName, cancellable) { + return this.gConnection.call(this.gName, + this.gObjectPath, 'org.freedesktop.DBus.Properties', 'Get', + GLib.Variant.new('(ss)', [this.gInterfaceName, propertyName]), + DBusProxy.TUPLE_VARIANT_TYPE, Gio.DBusCallFlags.NONE, -1, + cancellable); + } + + getProperties(cancellable) { + return this.gConnection.call(this.gName, + this.gObjectPath, 'org.freedesktop.DBus.Properties', 'GetAll', + GLib.Variant.new('(s)', [this.gInterfaceName]), + GLib.VariantType.new('(a{sv})'), Gio.DBusCallFlags.NONE, -1, + cancellable); + } +}); diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/extension.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/extension.js new file mode 100644 index 0000000..16e6596 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/extension.js @@ -0,0 +1,89 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import * as Extension from 'resource:///org/gnome/shell/extensions/extension.js'; + +import * as StatusNotifierWatcher from './statusNotifierWatcher.js'; +import * as Interfaces from './interfaces.js'; +import * as TrayIconsManager from './trayIconsManager.js'; +import * as Util from './util.js'; +import {SettingsManager} from './settingsManager.js'; + +export default class DashToDockExtension extends Extension.Extension { + constructor(...args) { + super(...args); + + Util.Logger.init(this); + Interfaces.initialize(this); + + this._isEnabled = false; + this._statusNotifierWatcher = null; + this._watchDog = new Util.NameWatcher(StatusNotifierWatcher.WATCHER_BUS_NAME); + this._watchDog.connect('vanished', () => this._maybeEnableAfterNameAvailable()); + + // HACK: we want to leave the watchdog alive when disabling the extension, + // but if we are being reloaded, we destroy it since it could be considered + // a leak and spams our log, too. + /* eslint-disable no-undef */ + if (typeof global['--appindicator-extension-on-reload'] === 'function') + global['--appindicator-extension-on-reload'](); + + global['--appindicator-extension-on-reload'] = () => { + Util.Logger.debug('Reload detected, destroying old watchdog'); + this._watchDog.destroy(); + this._watchDog = null; + }; + /* eslint-enable no-undef */ + } + + enable() { + this._isEnabled = true; + SettingsManager.initialize(this); + Util.tryCleanupOldIndicators(); + this._maybeEnableAfterNameAvailable(); + TrayIconsManager.TrayIconsManager.initialize(); + } + + disable() { + this._isEnabled = false; + TrayIconsManager.TrayIconsManager.destroy(); + + if (this._statusNotifierWatcher !== null) { + this._statusNotifierWatcher.destroy(); + this._statusNotifierWatcher = null; + } + + SettingsManager.destroy(); + } + + // FIXME: when entering/leaving the lock screen, the extension might be + // enabled/disabled rapidly. + // This will create very bad side effects in case we were not done unowning + // the name while trying to own it again. Since g_bus_unown_name doesn't + // fire any callback when it's done, we need to monitor the bus manually + // to find out when the name vanished so we can reclaim it again. + _maybeEnableAfterNameAvailable() { + // by the time we get called whe might not be enabled + if (!this._isEnabled || this._statusNotifierWatcher) + return; + + if (this._watchDog.nameAcquired && this._watchDog.nameOnBus) + return; + + this._statusNotifierWatcher = new StatusNotifierWatcher.StatusNotifierWatcher( + this._watchDog); + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/iconCache.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/iconCache.js new file mode 100644 index 0000000..b775307 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/iconCache.js @@ -0,0 +1,179 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; + +import * as PromiseUtils from './promiseUtils.js'; +import * as Util from './util.js'; + +// The icon cache caches icon objects in case they're reused shortly aftwerwards. +// This is necessary for some indicators like skype which rapidly switch between serveral icons. +// Without caching, the garbage collection would never be able to handle the amount of new icon data. +// If the lifetime of an icon is over, the cache will destroy the icon. (!) +// The presence of active icons will extend the lifetime. + +const GC_INTERVAL = 100; // seconds +const LIFETIME_TIMESPAN = 120; // seconds + +// how to use: see IconCache.add, IconCache.get +export class IconCache { + constructor() { + this._cache = new Map(); + this._activeIcons = Object.create(null); + this._lifetime = new Map(); // we don't want to attach lifetime to the object + } + + add(id, icon) { + if (!(icon instanceof Gio.Icon)) { + Util.Logger.critical('IconCache: Only Gio.Icons are supported'); + return null; + } + + if (!id) { + Util.Logger.critical('IconCache: Invalid ID provided'); + return null; + } + + const oldIcon = this._cache.get(id); + if (!oldIcon || !oldIcon.equals(icon)) { + Util.Logger.debug(`IconCache: adding ${id}: ${icon}`); + this._cache.set(id, icon); + } else { + icon = oldIcon; + } + + this._renewLifetime(id); + this._checkGC(); + + return icon; + } + + updateActive(iconType, gicon, isActive) { + if (!gicon) + return; + + const previousActive = this._activeIcons[iconType]; + + if (isActive && [...this._cache.values()].some(icon => icon === gicon)) + this._activeIcons[iconType] = gicon; + else if (previousActive === gicon) + delete this._activeIcons[iconType]; + else + return; + + if (previousActive) { + this._cache.forEach((icon, id) => { + if (icon === previousActive) + this._renewLifetime(id); + }); + } + } + + _remove(id) { + Util.Logger.debug(`IconCache: removing ${id}`); + + this._cache.delete(id); + this._lifetime.delete(id); + } + + _renewLifetime(id) { + this._lifetime.set(id, new Date().getTime() + LIFETIME_TIMESPAN * 1000); + } + + forceDestroy(id) { + const gicon = this._cache.has(id); + if (gicon) { + Object.keys(this._activeIcons).forEach(iconType => + this.updateActive(iconType, gicon, false)); + this._remove(id); + this._checkGC(); + } + } + + // marks all the icons as removable, if something doesn't claim them before + weakClear() { + this._activeIcons = Object.create(null); + this._checkGC(); + } + + // removes everything from the cache + clear() { + this._activeIcons = Object.create(null); + this._cache.forEach((_icon, id) => this._remove(id)); + this._checkGC(); + } + + // returns an object from the cache, or null if it can't be found. + get(id) { + const icon = this._cache.get(id); + if (icon) { + Util.Logger.debug(`IconCache: retrieving ${id}: ${icon}`); + this._renewLifetime(id); + return icon; + } + + return null; + } + + async _checkGC() { + const cacheIsEmpty = this._cache.size === 0; + + if (!cacheIsEmpty && !this._gcTimeout) { + Util.Logger.debug('IconCache: garbage collector started'); + let anyUnusedInCache = false; + this._gcTimeout = new PromiseUtils.TimeoutSecondsPromise(GC_INTERVAL, + GLib.PRIORITY_LOW); + try { + await this._gcTimeout; + anyUnusedInCache = this._gc(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'IconCache: garbage collector'); + } finally { + delete this._gcTimeout; + } + + if (anyUnusedInCache) + this._checkGC(); + } else if (cacheIsEmpty && this._gcTimeout) { + Util.Logger.debug('IconCache: garbage collector stopped'); + this._gcTimeout.cancel(); + } + } + + _gc() { + const time = new Date().getTime(); + let anyUnused = false; + + this._cache.forEach((icon, id) => { + if (Object.values(this._activeIcons).includes(icon)) { + Util.Logger.debug(`IconCache: ${id} is in use.`); + } else if (this._lifetime.get(id) < time) { + this._remove(id); + } else { + anyUnused = true; + Util.Logger.debug(`IconCache: ${id} survived this round.`); + } + }); + + return anyUnused; + } + + destroy() { + this.clear(); + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js new file mode 100644 index 0000000..5ef516c --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/indicatorStatusIcon.js @@ -0,0 +1,592 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; + +import * as AppDisplay from 'resource:///org/gnome/shell/ui/appDisplay.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Panel from 'resource:///org/gnome/shell/ui/panel.js'; +import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; + +import * as AppIndicator from './appIndicator.js'; +import * as PromiseUtils from './promiseUtils.js'; +import * as SettingsManager from './settingsManager.js'; +import * as Util from './util.js'; +import * as DBusMenu from './dbusMenu.js'; + +const DEFAULT_ICON_SIZE = Panel.PANEL_ICON_SIZE || 16; + +export function addIconToPanel(statusIcon) { + if (!(statusIcon instanceof BaseStatusIcon)) + throw TypeError(`Unexpected icon type: ${statusIcon}`); + + const settings = SettingsManager.getDefaultGSettings(); + const indicatorId = `appindicator-${statusIcon.uniqueId}`; + + const currentIcon = Main.panel.statusArea[indicatorId]; + if (currentIcon) { + if (currentIcon !== statusIcon) + currentIcon.destroy(); + + Main.panel.statusArea[indicatorId] = null; + } + + Main.panel.addToStatusArea(indicatorId, statusIcon, 1, + settings.get_string('tray-pos')); + + Util.connectSmart(settings, 'changed::tray-pos', statusIcon, () => + addIconToPanel(statusIcon)); +} + +export function getTrayIcons() { + return Object.values(Main.panel.statusArea).filter( + i => i instanceof IndicatorStatusTrayIcon); +} + +export function getAppIndicatorIcons() { + return Object.values(Main.panel.statusArea).filter( + i => i instanceof IndicatorStatusIcon); +} + +export const BaseStatusIcon = GObject.registerClass( +class IndicatorBaseStatusIcon extends PanelMenu.Button { + _init(menuAlignment, nameText, iconActor, dontCreateMenu) { + super._init(menuAlignment, nameText, dontCreateMenu); + + const settings = SettingsManager.getDefaultGSettings(); + Util.connectSmart(settings, 'changed::icon-opacity', this, this._updateOpacity); + this.connect('notify::hover', () => this._onHoverChanged()); + + if (!super._onDestroy) + this.connect('destroy', () => this._onDestroy()); + + this._box = new St.BoxLayout({style_class: 'panel-status-indicators-box'}); + this.add_child(this._box); + + this._setIconActor(iconActor); + this._showIfReady(); + } + + _setIconActor(icon) { + if (!(icon instanceof Clutter.Actor)) + throw new Error(`${icon} is not a valid actor`); + + if (this._icon && this._icon !== icon) + this._icon.destroy(); + + this._icon = icon; + this._updateEffects(); + this._monitorIconEffects(); + + if (this._icon) { + this._box.add_child(this._icon); + const id = this._icon.connect('destroy', () => { + this._icon.disconnect(id); + this._icon = null; + this._monitorIconEffects(); + }); + } + } + + _onDestroy() { + if (this._icon) + this._icon.destroy(); + + if (super._onDestroy) + super._onDestroy(); + } + + isReady() { + throw new GObject.NotImplementedError('isReady() in %s'.format(this.constructor.name)); + } + + get icon() { + return this._icon; + } + + get uniqueId() { + throw new GObject.NotImplementedError('uniqueId in %s'.format(this.constructor.name)); + } + + _showIfReady() { + this.visible = this.isReady(); + } + + _onHoverChanged() { + if (this.hover) { + this.opacity = 255; + if (this._icon) + this._icon.remove_effect_by_name('desaturate'); + } else { + this._updateEffects(); + } + } + + _updateOpacity() { + const settings = SettingsManager.getDefaultGSettings(); + const userValue = settings.get_user_value('icon-opacity'); + if (userValue) + this.opacity = userValue.unpack(); + else + this.opacity = 255; + } + + _updateEffects() { + this._updateOpacity(); + + if (this._icon) { + this._updateSaturation(); + this._updateBrightnessContrast(); + } + } + + _monitorIconEffects() { + const settings = SettingsManager.getDefaultGSettings(); + const monitoring = !!this._iconSaturationIds; + + if (!this._icon && monitoring) { + Util.disconnectSmart(settings, this, this._iconSaturationIds); + delete this._iconSaturationIds; + + Util.disconnectSmart(settings, this, this._iconBrightnessIds); + delete this._iconBrightnessIds; + + Util.disconnectSmart(settings, this, this._iconContrastIds); + delete this._iconContrastIds; + } else if (this._icon && !monitoring) { + this._iconSaturationIds = + Util.connectSmart(settings, 'changed::icon-saturation', this, + this._updateSaturation); + this._iconBrightnessIds = + Util.connectSmart(settings, 'changed::icon-brightness', this, + this._updateBrightnessContrast); + this._iconContrastIds = + Util.connectSmart(settings, 'changed::icon-contrast', this, + this._updateBrightnessContrast); + } + } + + _updateSaturation() { + const settings = SettingsManager.getDefaultGSettings(); + const desaturationValue = settings.get_double('icon-saturation'); + let desaturateEffect = this._icon.get_effect('desaturate'); + + if (desaturationValue > 0) { + if (!desaturateEffect) { + desaturateEffect = new Clutter.DesaturateEffect(); + this._icon.add_effect_with_name('desaturate', desaturateEffect); + } + desaturateEffect.set_factor(desaturationValue); + } else if (desaturateEffect) { + this._icon.remove_effect(desaturateEffect); + } + } + + _updateBrightnessContrast() { + const settings = SettingsManager.getDefaultGSettings(); + const brightnessValue = settings.get_double('icon-brightness'); + const contrastValue = settings.get_double('icon-contrast'); + let brightnessContrastEffect = this._icon.get_effect('brightness-contrast'); + + if (brightnessValue !== 0 | contrastValue !== 0) { + if (!brightnessContrastEffect) { + brightnessContrastEffect = new Clutter.BrightnessContrastEffect(); + this._icon.add_effect_with_name('brightness-contrast', brightnessContrastEffect); + } + brightnessContrastEffect.set_brightness(brightnessValue); + brightnessContrastEffect.set_contrast(contrastValue); + } else if (brightnessContrastEffect) { + this._icon.remove_effect(brightnessContrastEffect); + } + } +}); + +/* + * IndicatorStatusIcon implements an icon in the system status area + */ +export const IndicatorStatusIcon = GObject.registerClass( +class IndicatorStatusIcon extends BaseStatusIcon { + _init(indicator) { + super._init(0.5, indicator.accessibleName, + new AppIndicator.IconActor(indicator, DEFAULT_ICON_SIZE)); + this._indicator = indicator; + + this._lastClickTime = -1; + this._lastClickX = -1; + this._lastClickY = -1; + + this._box.add_style_class_name('appindicator-box'); + + Util.connectSmart(this._indicator, 'ready', this, this._showIfReady); + Util.connectSmart(this._indicator, 'menu', this, this._updateMenu); + Util.connectSmart(this._indicator, 'label', this, this._updateLabel); + Util.connectSmart(this._indicator, 'status', this, this._updateStatus); + Util.connectSmart(this._indicator, 'reset', this, () => { + this._updateStatus(); + this._updateLabel(); + }); + Util.connectSmart(this._indicator, 'accessible-name', this, () => + this.set_accessible_name(this._indicator.accessibleName)); + Util.connectSmart(this._indicator, 'destroy', this, () => this.destroy()); + + this.connect('notify::visible', () => this._updateMenu()); + + this._showIfReady(); + } + + _onDestroy() { + if (this._menuClient) { + this._menuClient.disconnect(this._menuReadyId); + this._menuClient.destroy(); + this._menuClient = null; + } + + super._onDestroy(); + } + + get uniqueId() { + return this._indicator.uniqueId; + } + + isReady() { + return this._indicator && this._indicator.isReady; + } + + _updateLabel() { + const {label} = this._indicator; + if (label) { + if (!this._label || !this._labelBin) { + this._labelBin = new St.Bin({ + yAlign: Clutter.ActorAlign.CENTER, + }); + this._label = new St.Label(); + this._labelBin.add_actor(this._label); + this._box.add_actor(this._labelBin); + } + this._label.set_text(label); + if (!this._box.contains(this._labelBin)) + this._box.add_actor(this._labelBin); // FIXME: why is it suddenly necessary? + } else if (this._label) { + this._labelBin.destroy_all_children(); + this._box.remove_actor(this._labelBin); + this._labelBin.destroy(); + delete this._labelBin; + delete this._label; + } + } + + _updateStatus() { + const wasVisible = this.visible; + this.visible = this._indicator.status !== AppIndicator.SNIStatus.PASSIVE; + + if (this.visible !== wasVisible) + this._indicator.checkAlive().catch(logError); + } + + _updateMenu() { + if (this._menuClient) { + this._menuClient.disconnect(this._menuReadyId); + this._menuClient.destroy(); + this._menuClient = null; + this.menu.removeAll(); + } + + if (this.visible && this._indicator.menuPath) { + this._menuClient = new DBusMenu.Client(this._indicator.busName, + this._indicator.menuPath, this._indicator); + + if (this._menuClient.isReady) + this._menuClient.attachToMenu(this.menu); + + this._menuReadyId = this._menuClient.connect('ready-changed', () => { + if (this._menuClient.isReady) + this._menuClient.attachToMenu(this.menu); + else + this._updateMenu(); + }); + } + } + + _showIfReady() { + if (!this.isReady()) + return; + + this._updateLabel(); + this._updateStatus(); + this._updateMenu(); + } + + _updateClickCount(event) { + const [x, y] = event.get_coords(); + const time = event.get_time(); + const {doubleClickDistance, doubleClickTime} = + Clutter.Settings.get_default(); + + if (time > (this._lastClickTime + doubleClickTime) || + (Math.abs(x - this._lastClickX) > doubleClickDistance) || + (Math.abs(y - this._lastClickY) > doubleClickDistance)) + this._clickCount = 0; + + this._lastClickTime = time; + this._lastClickX = x; + this._lastClickY = y; + + this._clickCount = (this._clickCount % 2) + 1; + + return this._clickCount; + } + + _maybeHandleDoubleClick(event) { + if (this._indicator.supportsActivation === false) + return Clutter.EVENT_PROPAGATE; + + if (event.get_button() !== Clutter.BUTTON_PRIMARY) + return Clutter.EVENT_PROPAGATE; + + if (this._updateClickCount(event) === 2) { + this._indicator.open(...event.get_coords(), event.get_time()); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + async _waitForDoubleClick() { + const {doubleClickTime} = Clutter.Settings.get_default(); + this._waitDoubleClickPromise = new PromiseUtils.TimeoutPromise( + doubleClickTime); + + try { + await this._waitDoubleClickPromise; + this.menu.toggle(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + throw e; + } finally { + delete this._waitDoubleClickPromise; + } + } + + vfunc_event(event) { + if (this.menu.numMenuItems && event.type() === Clutter.EventType.TOUCH_BEGIN) + this.menu.toggle(); + + return Clutter.EVENT_PROPAGATE; + } + + vfunc_button_press_event(event) { + if (this._waitDoubleClickPromise) + this._waitDoubleClickPromise.cancel(); + + // if middle mouse button clicked send SecondaryActivate dbus event and do not show appindicator menu + if (event.get_button() === Clutter.BUTTON_MIDDLE) { + if (Main.panel.menuManager.activeMenu) + Main.panel.menuManager._closeMenu(true, Main.panel.menuManager.activeMenu); + this._indicator.secondaryActivate(event.get_time(), ...event.get_coords()); + return Clutter.EVENT_STOP; + } + + if (event.get_button() === Clutter.BUTTON_SECONDARY) { + this.menu.toggle(); + return Clutter.EVENT_PROPAGATE; + } + + const doubleClickHandled = this._maybeHandleDoubleClick(event); + if (doubleClickHandled === Clutter.EVENT_PROPAGATE && + event.get_button() === Clutter.BUTTON_PRIMARY && + this.menu.numMenuItems) { + if (this._indicator.supportsActivation !== false) + this._waitForDoubleClick().catch(logError); + else + this.menu.toggle(); + } + + return Clutter.EVENT_PROPAGATE; + } + + vfunc_button_release_event(event) { + if (!this._indicator.supportsActivation) + return this._maybeHandleDoubleClick(event); + + return Clutter.EVENT_PROPAGATE; + } + + vfunc_scroll_event(event) { + // Since Clutter 1.10, clutter will always send a smooth scrolling event + // with explicit deltas, no matter what input device is used + // In fact, for every scroll there will be a smooth and non-smooth scroll + // event, and we can choose which one we interpret. + if (event.get_scroll_direction() === Clutter.ScrollDirection.SMOOTH) { + const [dx, dy] = event.get_scroll_delta(); + + this._indicator.scroll(dx, dy); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } +}); + +export const IndicatorStatusTrayIcon = GObject.registerClass( +class IndicatorTrayIcon extends BaseStatusIcon { + _init(icon) { + super._init(0.5, icon.wm_class, icon, {dontCreateMenu: true}); + Util.Logger.debug(`Adding legacy tray icon ${this.uniqueId}`); + this._box.add_style_class_name('appindicator-trayicons-box'); + this.add_style_class_name('appindicator-icon'); + this.add_style_class_name('tray-icon'); + + this.connect('button-press-event', (_actor, _event) => { + this.add_style_pseudo_class('active'); + return Clutter.EVENT_PROPAGATE; + }); + this.connect('button-release-event', (_actor, event) => { + this._icon.click(event); + this.remove_style_pseudo_class('active'); + return Clutter.EVENT_PROPAGATE; + }); + this.connect('key-press-event', (_actor, event) => { + this.add_style_pseudo_class('active'); + this._icon.click(event); + return Clutter.EVENT_PROPAGATE; + }); + this.connect('key-release-event', (_actor, event) => { + this._icon.click(event); + this.remove_style_pseudo_class('active'); + return Clutter.EVENT_PROPAGATE; + }); + + Util.connectSmart(this._icon, 'destroy', this, () => { + icon.clear_effects(); + this.destroy(); + }); + + const settings = SettingsManager.getDefaultGSettings(); + Util.connectSmart(settings, 'changed::icon-size', this, this._updateIconSize); + + const themeContext = St.ThemeContext.get_for_stage(global.stage); + Util.connectSmart(themeContext, 'notify::scale-factor', this, () => + this._updateIconSize()); + + this._updateIconSize(); + } + + _onDestroy() { + Util.Logger.debug(`Destroying legacy tray icon ${this.uniqueId}`); + + if (this._waitDoubleClickPromise) + this._waitDoubleClickPromise.cancel(); + + super._onDestroy(); + } + + isReady() { + return !!this._icon; + } + + get uniqueId() { + return `legacy:${this._icon.wm_class}:${this._icon.pid}`; + } + + vfunc_navigate_focus(from, direction) { + this.grab_key_focus(); + return super.vfunc_navigate_focus(from, direction); + } + + _getSimulatedButtonEvent(touchEvent) { + const event = Clutter.Event.new(Clutter.EventType.BUTTON_RELEASE); + event.set_button(1); + event.set_time(touchEvent.get_time()); + event.set_flags(touchEvent.get_flags()); + event.set_stage(global.stage); + event.set_source(touchEvent.get_source()); + event.set_coords(...touchEvent.get_coords()); + event.set_state(touchEvent.get_state()); + return event; + } + + vfunc_touch_event(event) { + // Under X11 we rely on emulated pointer events + if (!imports.gi.Meta.is_wayland_compositor()) + return Clutter.EVENT_PROPAGATE; + + const slot = event.get_event_sequence().get_slot(); + + if (!this._touchPressSlot && + event.get_type() === Clutter.EventType.TOUCH_BEGIN) { + this.add_style_pseudo_class('active'); + this._touchButtonEvent = this._getSimulatedButtonEvent(event); + this._touchPressSlot = slot; + this._touchDelayPromise = new PromiseUtils.TimeoutPromise( + AppDisplay.MENU_POPUP_TIMEOUT); + this._touchDelayPromise.then(() => { + delete this._touchDelayPromise; + delete this._touchPressSlot; + this._touchButtonEvent.set_button(3); + this._icon.click(this._touchButtonEvent); + this.remove_style_pseudo_class('active'); + }); + } else if (event.get_type() === Clutter.EventType.TOUCH_END && + this._touchPressSlot === slot) { + delete this._touchPressSlot; + delete this._touchButtonEvent; + if (this._touchDelayPromise) { + this._touchDelayPromise.cancel(); + delete this._touchDelayPromise; + } + + this._icon.click(this._getSimulatedButtonEvent(event)); + this.remove_style_pseudo_class('active'); + } else if (event.get_type() === Clutter.EventType.TOUCH_UPDATE && + this._touchPressSlot === slot) { + this.add_style_pseudo_class('active'); + this._touchButtonEvent = this._getSimulatedButtonEvent(event); + } + + return Clutter.EVENT_PROPAGATE; + } + + vfunc_leave_event(event) { + this.remove_style_pseudo_class('active'); + + if (this._touchDelayPromise) { + this._touchDelayPromise.cancel(); + delete this._touchDelayPromise; + } + + return super.vfunc_leave_event(event); + } + + _updateIconSize() { + const settings = SettingsManager.getDefaultGSettings(); + const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage); + let iconSize = settings.get_int('icon-size'); + + if (iconSize <= 0) + iconSize = DEFAULT_ICON_SIZE; + + this.height = -1; + this._icon.set({ + width: iconSize * scaleFactor, + height: iconSize * scaleFactor, + xAlign: Clutter.ActorAlign.CENTER, + yAlign: Clutter.ActorAlign.CENTER, + }); + } +}); diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml new file mode 100644 index 0000000..e98e04e --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/DBusMenu.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml new file mode 100644 index 0000000..8c36294 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierItem.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml new file mode 100644 index 0000000..e450d04 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces-xml/StatusNotifierWatcher.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces.js new file mode 100644 index 0000000..0a97907 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/interfaces.js @@ -0,0 +1,52 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +export let StatusNotifierItem = null; +export let StatusNotifierWatcher = null; +export let DBusMenu = null; + +// loads a xml file into an in-memory string +function loadInterfaceXml(extension, filename) { + const interfacesDir = extension.dir.get_child('interfaces-xml'); + const file = interfacesDir.get_child(filename); + const [result, contents] = imports.gi.GLib.file_get_contents(file.get_path()); + + if (result) { + // HACK: The "" + trick is important as hell because file_get_contents returns + // an object (WTF?) but Gio.makeProxyWrapper requires `typeof() === "string"` + // Otherwise, it will try to check `instanceof XML` and fail miserably because there + // is no `XML` on very recent SpiderMonkey releases (or, if SpiderMonkey is old enough, + // will spit out a TypeError soon). + let nodeContents = contents; + if (contents instanceof Uint8Array) + nodeContents = imports.byteArray.toString(contents); + return `${nodeContents}`; + } else { + throw new Error(`AppIndicatorSupport: Could not load file: ${filename}`); + } +} + +export function initialize(extension) { + StatusNotifierItem = loadInterfaceXml(extension, 'StatusNotifierItem.xml'); + StatusNotifierWatcher = loadInterfaceXml(extension, 'StatusNotifierWatcher.xml'); + DBusMenu = loadInterfaceXml(extension, 'DBusMenu.xml'); +} + +export function destroy() { + StatusNotifierItem = null; + StatusNotifierWatcher = null; + DBusMenu = null; +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/cs/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..c66d72ef88af4d23c37db2b16f784624bb96bd87 GIT binary patch literal 2225 zcma)+%Wqpn6o&^|9yJtbdA7U`n<^2#spGVzb>kM=G*!|zvC`CH$>e%s550HBni)H_ zg#=>F0)#-SxBB-i3w{O`z^_39zXu-$e+3^0e*<3w{{Wu^#jXMOf^R@P z2<`!=bIyVmQL;2rQiOo#=Lzb}ETpMad_8pwHm1U?0RmGfKh zb;v)0oX79^{-62&Um)8%fFxc3r$DaDJcyEs1mt)vh@+wn^7#)yuK$-H>wN=qJU@ZQ z!Jk1szYD=J3*Zd651a;TAouoj5NV2+Fki%EpKOnN!1a3?a~~!T?i2fjig*qaF0$I9 zM44|Os};w`gZ(~_$$cCSt_=_N&3SQ6M+f(g`#3tXd-F~R$^>l<&Bgdxkf{=9oo+OP zQQnU%X)M*IOwr^-@i3+GdYP~q7w4d@oH%DqaMFj(#ZJfDKG-z#<;A&ypyAky8RcbY zJDJ5Y@KxOIYAe8O<#kVq%~kGi7~9F?<0Id+p_aa`l6K`gRb=nnaS!_u*1<6_a(l_NI8CaTu!ESt>@r&g6yCRSc7sa7Im zMbdOBD?k<3jrg<@z*>KZ3-9wvYLNd_}lcMvY>g@X}4$)jaswwAjkIGa)Zq|k9 zvTx{mbhh2_(Xz#Mbun5}9nFWL85vZWO2twsDjtoBC#hI0Pac1>SSl6^mvE0FxHLZD z29#+=8m$sqYI}8!rc7*TWHXLz`0+H>L9`RIV=1Mklt;aWH6Ibl$|aj=NK)6Dv9tstGP^ z63tqd`ZAeTCi=?MDH<#*V`HXLnH;AxRHQPEnbVaL9Xdp2x^k2^C$U)?KUH`)x}c20 z-REqpR@HT&sfpsmWI-(S*L$i%`kKW>@zieFo^|H#P1C=fih1dI?(o)XihS6(q3UgM zariuki=9*w)E=e>{zPrNxL9A2uA}(-zdq2>SVc`G2-=6CsZnvLp8t{XW8Hq7I)ewR z;-*-#J>^5Y)umMRbY@PbdWUT?k`leK9`4>0c(}Dhb*#APx}kqN#{<5zcU!d^H)I`Y zjc8v@`?o#JaJD1cRW4}9CM#XBAe&Bds}j9pv1R(V@;-I)x}}@pQjby_bZ6r`QENGg zpssWSsskfF$WBm)=~`V4GQ_szCO|t&1=*6vxBhxAXI`jhSDftGj(pGvq8+^5yb%AP VT(foSY=@)o$!xLCIcLgU@efOqPPza9 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/de/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..6edb41980de1d45d7c86ce38d2c0802fcab21cc5 GIT binary patch literal 949 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ja/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..d22309b6758db3bc2bcafd3abb98362948c6a804 GIT binary patch literal 972 zcmZ{i%}>-o7{*7%uO?ndOhkzpFDlVlyK4|AVhmpf17U-!A>PLBP=@qlGaU$v7t_6% zsNhB8K`(}2Nc=buWhL;hcc&PuAH7G)^XBF2n@S7>-zja1ztb}qw zY%e!A>ABWHM#YpS<0WoOUEZ^t>+I|x6NyJsP`pg110@&Wg}aoy((ygmBY73f2wSs} zBD*J42oDxDCPXPvnvI2yEVBMtCuD^HkMrYfLd?k|i4AfsESfX3UCiuZ*)E#PS=pXT zX3jK?Q5=?GOFwkE4iw%T<-Rk^9l;8MdltPdJ+Dmf*eIkWy*`tKnXnPwbUZHIj2(D4 z{=R0>8E%Y?j1BF0#0NH#4~sLfLb(JlTFmP!C6`OTm)g@@g+lk;Eex~noxH@-lu&HQ zw}Ybe9gB89klL7Jw}db72NJLOKoq5>xr~{)YUEw~7?%wP$_p*(m(noo%l6X7w9o7{ z$oE%^^^KMNTcB3`P+$H?;%F_d;CdWaR^#X^Ci|z?w^mWAwzXKRK51m^X@xj?8%N8{ za?$5>zR}Fw)fM&i6;i7{`|)ULx4G@+OdF%Vv_?`x@8im5TzL~m)i_#@EAL>G`~jg$ B7a{-v literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ko/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..6d33af8d4f006780fdcf914f604e6d9ed568b8de GIT binary patch literal 2373 zcma)+-)~e!6vszJg;o54zY)#R#DprhY!@o9P?g_SK^iE=7+$Ws)7`6k@7>&cD=mqk z)-9<}LkUzDvo$pqv7`@+R$OB1i}A@9ee;Qg7a#V{{s|_2&-Ct(q7WvT{mhv=Gv|BG znb~WrmQ4t>XVBN6FS|pCHc)}w#_ts35%3Hc122FCmce_#Kf&eTU*NOgb?|;r+_lJ+ z;BzongDb#o5%+*k!|nlBfjN-(9S2#*SY*$D%V1vt?*)GbSA)NTE5U2vL!bih1OEZn zgDbGb>mbKvU=f$}Np!YRCh<5r*Fft9A4ccK=V@Qyqv&w0bBh$| zTmo7X$HR^NK8DV9thWcE0pBaG1In_baSWWl)~4^x_8^c>V7U%;r||QZX-l!gvohJh zk-pzDq_wu?B+2M#kFO)!97qyIZDJ>OOHb@{oxn5wpfR}F@_s*XZSBh!I~NTLj(xF9 z`exAYnOe)DulQ0|O@VskTgRkmj3R$^ZChS$Yx!n5l(zfR;x$-Y1w;BCQRoi{j4bau9h172Es#K-UvJ+5!*Xc`lq?312! zQqmU(Wj<%7B$>`2=>%v6lxj%{^?67Y6xqC zPb7@^MkA4+j!nspoiD|=#N)AjxLgKwIDU?+k|Z@br=})qle4rfZA!a8w<+;j#x|{7 zd&;$6!FngtZ)T*?D^2^hS10KMH@3ff|L$fqxaBdv-H6uc9n3>uAP0i=sGj9)rBv4R zd>M2d?%iW-ZmwgUD7CTMNx5mcm+6 zslp1!hN9Q1rq8H}^8yp$Rcz3>^@ibD_2rEEYMj)?A>_O1T&P=ox?F%Ds+Pwgqwa?*V?USK!7;)+xEIq5Sl+9 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/nl/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..9d81088f9018447ac7bc68c2d1d3838ade4b8182 GIT binary patch literal 1024 zcmaiy&2AGh6on0xzd>xeD;fDk(~b2D)>V@I}AHQ^oD z!HSn)0~?-%1rl!nZ@`&0B`TuA($O~_U;7^W-tV(B?-@n_-Gg%IHuM!L$4`huzo2W- z>?OwTgLB|KSO@QdufZAc2)qNn1+Ri1z&Y?EcnE!lIOg+L7`p|of#rG6!18|qJ^<7CGsMR(fUhBqTh4&Q5|Ar^a-)JW0}6s<2(^dbU51 ziSnbrmTQZP6?Ra1WF0F&Q1>bT&nO8duuwk`)^| zF{#p7%t^gJ46YVE9#PkV^4?AZ37{vqy0&Jz_!vafVXIc%`s7E=iE literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/oc/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..fe31bca0df8328b560949fb09eab0882752c4161 GIT binary patch literal 2334 zcmb7^J8T?97{>=Nj~pa`Aw0rE9|DP3WSz5}Kyt=_=;lrijG<+k4s&2}auc&CJbw?{BV+k9{sM zp1}7wzOh?`m;kSXJU+QqhzGz=!3y{dNZ|M29pJCvUEo#lN$@xD9#GuYpgG`>&xJs;1%!~Sb&GYYv6t0b?|QR26z%YgiE)B zXF<06W$*>?Rd5FU5PTH84A$Z6FCg206i%_6Q{dC!D;CmP!(p1Xj9B};D=KJySU^_VHxKiqb$}%Ph zQKK8X?<3f;*|zJ4myPNyfTu5@cR9ZTgN1eb9)4HhyF_oe5^9Fs)6UdagQM ztyWf0TmkGbF6OM%=(U{d)=+bPx%g-U|C@F$+bU=&nLuuTU86LttTt8`hxVavD_pO* zAGEeJsPt;fpF}>inWeBJty6yP_102wW_XS@BYDB13DXGGQ=>C)YhP)`DA_nzN^Rn5 zWU_MU=B8(-)wv{Rgv~IGg+OSs)+MQbEGK#{ev%cSo~-t zMVKNOCPY}oN{-IsT9STrUKSvYbR_13mb7Zni6b*Q*p>Y&jZ&?n3^_~<(z_!g>Cho; z%|EvLt;*K*t?2h^Ut-^ZEV>JMv1_gDbvq*8ZGY(p4wtVW zY%Dv!_{>IV#e+im2`HnKsd%8NSv~{B9u*yrx$?pq?}V{JO!Oby=>crb{}-{jeNXaE2J literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pl/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..4536d6e544d3ce1aa88f91a8c2a42b75b2787d36 GIT binary patch literal 2451 zcma)+O>7%Q6vrn}ptyXt1qyB9acI?uZfZL%q)t=%l~!q*#A;HCDkL;rPwlb2Gpn63 zj;(U3D7|vw5LH6rL=PN*Ub$gQ91!Bbxe|y27mzr?k^ft-ous7_Bd>omvor7g-+MEA zZ~K-{1=?$P_Tbq$Cd4@S)Fb#q`wH9vehucpK1kqC;N#$5;8yS-@Ktc^QS1dNV*tL6 zaS0^wy^J4%Ph$L8#yj9{jK2Z5gFk|7=NFK5{h5uoZ4m-Lu?v4sfP2ALz5}X8o178MrVbL0LPz0aH_y+{ZI`4s;qu)W^zZK3o z?j0bBGgt5x_amFt|*7ZHedVdCa|F0nXAA?bz-wo~s$3di4oCEO_=kdpR zUj}*o6_E4h!I!{KK-PB$JPO_gS^pmy{|2Wp9>XNF1ae#sMA&IA*)IFsg@?6rJvbM9 z4!E(s7x2K8co7ezr<#$%)HjgE#Jae#Ue?C>;l^=sjkvLIt_#YWHm*6hXYp{|H@0VZ zaMF8aytansLi{btNQqO4uGYL!&TZ(@fk>MY1%-+HL5k#RiO?Gtr?Fcl;zVe%0%^G>Nspx{L%CJK#iR=vQkZZ-UO}xk! zGzSZyr$edj(Sr35)1K2^B}TnW@i4lhZ;o$xrwzq=RW-AzScckEHw~z*eGSWeO%b~m zbEcw0>1{%DXT$|{&5I@8<~u{)=dgI>$hcTaDk>p1!zRks=`;}Qc{+!HQRQI4Rdppe)vCH+#g-^nUJ5R%4>T_c&PcCHRLmDAgZ$y3Sfpa1l%IMd zU(Dxo7jQfS=rFFyRVmQ|s-fyMFOygs*U)sV!QvYJL}NHL;Z;~Wj;*HJl2sL~C>g=! za9pCenOm4!m|d5SOOfNtfY5@KP7F0(t@=YKl{V89)@0(8pSiShHkevp$D&ZzVAh1T zf`e6}sjJ%O%1zvdW^mRfkt>mj)1jN0oQ7L|GjqVq%oONt$`fAiE98&Q6lwqdw0msk zFn15|bpBYONY}5Y^ZDb2!{gXez?S0pbne~YoH7d6jx$oWs-itjP822zIk70aZAltQ z=}@aJt6H3w&e;C#-mUfC!|plaheD&3m>+%`hMk9OS=7pTX(}?Q*tQz@75_Oo{8WT> zIlM!gzZ94IH+nb2E-hVk{Tr=nZ;h5c65s8FHJU>_Vz|JL7Ql73fCJtbySQk(k;Y9( zGqGtj^qzeX`IE6}F}3m4XgGy?S`$kZ=X+~0E-O3OG$JD@mafsFuB^!ZM*?4rvZbbK ztGbv^Iyk2p?(>*_Gv;km)dzKXz);yDXr$?W2bU0UTPvfbxPp7pyBQO*Zkr}cJfU*y z)#yYzRV~3TMb>xP-`#Dq#5j?xaZ$QX2y05DhAFoF8@B%~&ybfP?pAkXAkw|HN*8G` zhVAle*d1&*I*qod$-78)1UwkgmSslzn$1h!?xwRJJ)euML0)w@h^KP BjOqXY literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/pt_BR/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..0bb1b446dc2ad303fb7e7d62bf7df7174a78aa80 GIT binary patch literal 1067 zcmZ{iPfrvv7{&+1KTP6<8){5BXxPN!?5-#bLUdUmBoH>6(HJkL>~v>KI_=nY3E)@I zqn^AN51zPq@Z@bjg%=Ng0{sR)okbQ2HhJ>vOy9om+xGb}IsPud2%(#hgswr~AU}RV z1o{nKh9<{?;1)OqPJ{E{4e$jx4(@^1!Pnp=@I5#MegNM>Um?H0d2yJp15V?67rYI= z0q=lcz^mYQ(8vD-uYhAH;^QYkq$+FrIu{^6e9Q!d@N)PZe)!(~8H0uqS1iVl(F>D=Ns*}>?FB8PcX`Lrjg&s)#-fLq zB)ae=Ep5rZ4colX#?dXCiIg__nWc%2N!Z$^ZN4Y`4%%dn$D|fj7ie^!Rv(gDJ&tNL zsz*_{iIY>*S5`8oO-$OX%eW+K=!7dSB`1$e`K#M~oGG32S#7dq>{D5vWt=9QMr;~g`TaD5sEN2SkQcea(cyR8GTqg83F5|q#R_&`kHz^z1EM*8k8&+DaDGV2m X1y7_78Vc+qzQ!viuzzehTX&N$XqrvQ)vk# zr3BQJ*hFI_8bxAEd@!-pZd-eO!WW4-^ABh=>Z6H?4?g<+oxOCou?7;I?#$<$nR9;E z?@WJKv*LY$<5`?f<6OH;h-UDCyKuoV0zLxX02AO%kU$0A1O5Tt2mS><11?*Ry&zR+ zfEzGRg9N@-;d$_W%s;8{3-Bq-N5M7V_aNK(3FNqbugq7j5CT834wrku_284>M({y! z54Z|E46X!EfLp=S;N9RyAm?!%+y;ILwt~NaPk`&N=qhTE0-G@Z4oN-){s}$@E{D_A zU=zrG)1V0+2MzFYg<}=|3?9S%RVWfO2XZ|pLH2hGd>;G_~d64(r0?&iLf)XWbhY2JPCAkf$-I^A zr=Gy-FWaH8w^;OpNK_U$ruNA$WtRzGm~E6yrOj=d%~sP)9D))Jxb#AY8<3`#9C1`8<2?tA_JwsE7rJQd_p*KtGD=hXNjpk(J5ax49Pk4-Op{mC(=gMzgUZ89 zleaNxW->|Iw263VZpb8)w3|%Aa?E#SQq@O0GcWK^?=W{JO*(h3yaj8_X6#8~PpWxG z;#K3I^d#Oou2Q!zb9O|nEv+r7gis%-5jCSqYBnCmbw-dJ^w>kKQC@Z1ho7Ir# zyYM(2zk^aua&<_}!`T@AN=S5EO~n@}zNqsZ!Htp{jo-q`OZ5>V1;S64rld+j{U0{3 z{zG3sg#ad40`Wk7Jm`4ClSytq7eY-{i%>2Be?pgD%2~~$8&l{CcV{@hB-B-wfUXVN zrlv^?a9oX{Om#hDU7~Myu(+{+*oY2r+$@bbq=!$mx~`_V0j%h2=*66xz?DT_r^F>5 zH0G_p-&2RUg23+o?nikx_7ZEBWOz9h=&$wnGowp zS!1^=e}1JWATES5!!?-Ku5}fbP`Xf`l?AJNK3a}w3#qSJ)w=U2q)v_a-AA4`w9Rp@ zHot`UfK+)^t~3+1=YW*x^>R^&Z(+? zxNXz70_|<|x6n5|A;dWN2;}zllR~@#ego#fn;?Ndg3o}zg3p4#f$xBSfX{>CsXn)Y z?_s(F+yWjOa0=Xsc?H}CHbJ)g3}hYG2lHFtCd}`Eo56eF4zLGq1s{ShfserFz`w!0 z;1(D?4ITkm_X+S2coy6Teh$6~-T_nO`U}WGYD8PZsyO^Kqyp8Re~k`Sy$1Vk_qfCOkX&?8WLAA$5#-NqPb2AFF`GZy(qCZN2%73&Z|(q zJU0~`Uft)U@i>p@w5iz`7otRm=XJ<^6rE8<;UZdBqKZ|q4zv&c&*OSWsD$y&{hgPTKN z>*hlTgZ~wNyN?-AyZ?Y~ts!P@*N0Zxp+v=9ojH_=-e8%Gq^5pwBjB%MQdC!#W!s8# zt`juskm0f5**mUSCu1bGYJ54-3dP9MU0to9o^89bJl_!)WrJ;y;mUGr-ty|Gbky#b z?tU_oOI=EA=-qDHwy36#RbA@c4_qR+@W=B+J literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/sr/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..a7770c5993c41ecfb767e84d1c9bc8f8e6867140 GIT binary patch literal 1132 zcmaiyO=}ZD7{^DgZ;M`ftBOn!OM{toH+@UDU4_0$p{1cl5l@cEG+o*3hTUmtYY#~e zdK3gjswf`37irqkm-rQCzkr`X@#?|18^Pge9w6?q?7zT6&@}P^* z9;C-th(h0>Gf?XZLau^sU^|!rFM|)j7H|Q)1U>;zgRj6g@Ga;;n@|_xYo`dg0Nw#p z;6sps&%s`BA3O(s2X+1rPv}RAy2Ux^ zizuR_qOe zINCmJGSHVGlVVnq=`zo|a_NulbZ>8h%xI4!7xE=K7liIIyl{_l&n@~T*dy66cr5ag zO%&KY5k~M}Q)5PygHW=msOT2h@O&|1xd6-AX*MGkT%E*5xfC`{n^r$F2bk4It!_J= zxnZVF)0o6%8Cv?0$7K+ra+3SSd0rGOCwS?%*rrivOifIU9XH3z8T!Rwj^&ohXd=a; z>?(WQ^#^Hwj)#$u&fVNN%N*zFUb7-(V}3p;xPH;5nMbZP?z7v%7kFB2A{&T;D``4q zrn-%+hZpzQco3E%oBHKO7&+a8^kC|A`qwSfaV*-=LG{#ZcPxwEq9#Q;;+szTXluyn z(bGnSIb@l1W#ym*Ic5Si7HZOoL4&9d>XWL)t7=zmt9`XjX{XxLQ~h_6_&bQ!)lOW+ z2TI}t#ZOgD?IV9Zt`N1R>Nn8 z4A{~AVDgv8CWHF(p`wNx)roo!S9P_811hMgH~VXN78OI=o|QT1_mLa$v40D_Vk?7&iAXg=L5lrpsP>;U4p(rVf=w8^cOk@ z^`0W+8rTQ+gJtjv_!8^^Ti|8z4R{v(2=;-WKns0=DAuJjoqUhM>sUVp!}r(V4e&h} z@_hzFy{}-%_Y(|#{sxh%9TUEvhQc@xU4*dR9vAi*dJlIbQ�`UQ6fbU_-Iw8AT+_e1#6Jo7c|IJF}Ww)cySkCn)RT=itB zDQe1ZAKOkwM@wWm^hl~!Y|}Mk)ed?Q1r>$Lbp!2@iZ$!9=GlCmtw`(8LrSA%*)-O( zMVG5Oo7&7Bs~UW+EU{(TQXz=V2rpBb@c1s{<18Md@kpAKZ}EilXaSdHD5+f`ys=SvH2zzs)Y%6JQaW0kUz|N`0q+-ucSDP;y4^%6FSq>dW+9ZGptUt&o%GrB z>KrQ{#)XQj(z01yGj*l&l$Kv9AH84?rIvW$&`QO~y7Dv`=EEaVrNFxvY|hw*OQ~+Q zmo6Kbpq*(psBgwR%VIh(K*N;xXK_sLQ%(_%^}Q@P*qY4lglYQ(pNu)(-R&Gf3@_nK zj5A4TA|lUP(ich#+BL*b+cq2MAs&KnYPhwqh(ik9L=*L3$^zy zR8kuTza5u$V}gAO*)qWcJIZR)wP-h_ZK1k!+K@+G_h?0%rZ7Lgi*>po@IOfM5934p AuK)l5 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/ta/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..9b6e5a67001bb1c57f47158e5685b0dca6e54e2e GIT binary patch literal 3523 zcmd6oONq+9)l0Vt5B2ccTnuskkmtP1UBGdcrW}lo4=9GFC~}~^KWLH zfIj0t;Nx&VqDUU^LfPjFxE3x!gs0&yxD`gQ3w{Mfk6)nZHIJaH@JYB0o`6rlAK_E* zcX$;pCuylM>Pxc~xC3T=IQy!_E)Uoq1ee-e@X)khpPYss&v&(5{ zYh&P6YmukJu;W#AHC(Tx>{UIz&#HzqR#LomTbr=ffwjr^qQD8G_RDO`N5jZ(7+;~a zDIK^t4y^$lI?-t0m{?L@y;EIH258PQ`|v7L$8 z=pbHISo25g73DOW+6k1?@V%$qcNTjm zH5B;!bvd#JEB3282MJuV7>#bL1JcK&xe`R=!y>@b)L`HG#&0g8B&|M|9cApy+hU&C*-QMB{ zjj*J=rWuC)#q}zE?eFr`ihi}KdwtKN#b)! z94D>!RT7^xQ(w!J8J|yDZzpjpi7zl_{rM!GkbxMo>U3&2>j%{(D_cjC_+rvJp2TO_ zC5b;X`79?>NB&-lnZGcY|@tZiFRa<2^Rf-~&$>5gB$zXZ`SBg+*dW zN$ki*ol{_!6ah1>XMvC2hlDquQEq@e(pObsrf6!x=6g5mU!2uw%ObELt6AfE|VQM9x_ zrQ1xW<0tKRDeDYQX5J@1BGMu0!OFLdC(=#Mqz;WaZqq(b&sBZ~K2F0j&1ZsjMscj!nJZDYO>@eXl&H+j(@~u% zX-!|w_=8DJGl^O!^6N~tCjv75G%IjN-WaBr9#tU`$(RcPQ8-NWJ<$;g)|paPK8q0! oGVwYN+W#}=jsT53z-DW-uu7m z`f>Z#PXyXa=r5q}*doLP_~?E3L;D(h8e9b{;5ta)ci@BI@8HAWpWut&miw_5q>=%+ z7xM;4;5#MX2Oq)wi;_3N-I(77w}an;?B^$t7(&x5akkAWw_ zC&77e8~83*1$*EF;AbG`@g?{=cnh2Ye+GAfJ7IJa8q`6~`v)Y+_P>Dy{snU0yRkXP zw-4M0R>8-?OC=@9dZge1@Z*wqLH73x$m{bk>6#OvN+ka4nui=Xh9q-iLGJ#<_ER+*q&Y(0Lzjd(b&nZk!|6g>&P^ns8$c zSda0>`ifKDE8}%yXts^Nc^NBl+Ul_DjdE^mNc&@L8WbFytR0|OUT+Xa6XFb3D=W?< z##`z9#^7G&ne$1U@8!gqVxqCJ6Vu8`pIMp5QtVaRyw!}6tw_;D70Py>npXA;KjgAB zO{^ECK~xwZo<8NiHw;E8<|C(!-V^sp|K#f9LKdQaysrlnU6s@LjMLG87mUZaEc#-YQn)~0H;%0(RP02vq; zaXlNfBwJR~;B$jajzzjJi zGO^*Lw)md8=i_%827p<-K#)h-V_TKg_q)jwCgKt zt3F>eKDo!T!KceADt3)<(G*k1LGW}s>4%w#vR<(|zcI%4oN+x(O_d_vJZ>z})KSi> zwH0kNDc{p3QtK;TTag?4+<55!-p0ARLWE-2O8S}4#Q3C)=>Ty=)F8f6Hos4G&mp@c zl2-OcFV|P1Y;7eIMdW&L17}tt6US*&D;d5&ts-r`8r+}@I*v4Q5=%)W=47ER?whL75wTv&N5CTM|hOh*z lYO>GA?%$5i%`eQ&6n@|t3;%n4o-g&K=c`FCBbQi;zX1j%wblRt literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/locale/zh_CN/LC_MESSAGES/AppIndicatorExtension.mo new file mode 100644 index 0000000000000000000000000000000000000000..df28844377825330994e4df73a664295880533b8 GIT binary patch literal 2338 zcma)+T}&KR6vuD1ez4Z6RqI!6u1}WMp<9raE@Difr6i@0c5UsGneJY8$L!2(W@hCh zF{!Z#B*2meDFRKPsX(d%jl{wNRbNefs!t~R;?8XMi5UB0?1TR^GYbo;O>c7dch8;k zeP;HL9b3O(Si7-zVQ<^QSSff9q&5BsV*zjiCp8)>>Uj?_|@@a4v_$;^= zdN(>x0+L=5d>I_|@8^B_Ctv;zqe}NSDUy%CrCYnzS zfuz?62Entw-0kxcco6qvU?q4Pq_`z8Lb-13lqcQ2giSqq4x4&O{%OI4y?~9->_u#p z=+=Z0xUm8DIyTLN7WI24_VXn8i+V+i-W9Z(;Wcp=&5G7`Y?^oRqPemoh7yZgnlR0x zBj2McT8K*r%7SlkRc;G$oJv`(&@5rFT3xdY*|gRU|EHcZEnVde5nVG`ttTk1P$x{; zN*S`H=$daF+!*SpCZA6TenQ0LNIMT3a=Yh;o2g_{H!S8WBs>M=)6QMrYYG&WpOjU> z+>@zv6rBn|{pt=MEM3>)ipG;ln@IRqNjaiuu?+%mRjfGFthnG5nl)%qB_dn8!5iw> z3DIJ)CNi_y3)~d2yt1;CH5pN1P&C&?vu-MGKh`b`(IO0?MTE&tiDW{K2rg@dGARQd zDQYTjG0<#eq>?@jm7QK|GCM6S-WrEHH)ppaZqLlPo=QZyoJq$z-l$sMM z6EB;IS1f>fF=VOHj44s+NGfJZVLikH%}vrN@v%aNQk`sx5HAmwS4hD_Qm~32s0>xS zy)Rf53ut8*)NArW>jm;#$%b&6)$%+&k6;s{BlHEYF84yte?1J@6?>R{qcgkk zEwg7Ao!LRMTsu94@tv;g_RL6jvBw_0<&1Z+&2s*$8}_AMcrEsY&-;FP49w;Rr=81F zPS0>|bl7v~49+^epECPF&)mrNKwoZZj(Jx0T*jVVCCPSYoE-<(7X^D6@^ zec@UKGp(QSuWRJCojUm0jt}t<13e4K++=5q6!7np2Xm@3D{d4x%wB7lA hc4??MF+Yn3o4Nl3_p$P>oVj^>(a*#gTC|tb>_3zVL{|U+ literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/metadata.json b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/metadata.json new file mode 100644 index 0000000..5024157 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/metadata.json @@ -0,0 +1,13 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell", + "gettext-domain": "AppIndicatorExtension", + "name": "AppIndicator and KStatusNotifierItem Support", + "settings-schema": "org.gnome.shell.extensions.appindicator", + "shell-version": [ + "45" + ], + "url": "https://github.com/ubuntu/gnome-shell-extension-appindicator", + "uuid": "appindicatorsupport@rgcjonas.gmail.com", + "version": 57 +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js new file mode 100644 index 0000000..ce4c3f3 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/pixmapsUtils.js @@ -0,0 +1,68 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +export function argbToRgba(src) { + const dest = new Uint8Array(src.length); + + for (let j = 0; j < src.length; j += 4) { + const srcAlpha = src[j]; + + dest[j] = src[j + 1]; /* red */ + dest[j + 1] = src[j + 2]; /* green */ + dest[j + 2] = src[j + 3]; /* blue */ + dest[j + 3] = srcAlpha; /* alpha */ + } + + return dest; +} + +export function getBestPixmap(pixmapsVariant, preferredSize) { + if (!pixmapsVariant) + throw new TypeError('null pixmapsVariant'); + + const pixmapsVariantsArray = new Array(pixmapsVariant.n_children()); + if (!pixmapsVariantsArray.length) + throw TypeError('Empty Icon found'); + + for (let i = 0; i < pixmapsVariantsArray.length; ++i) + pixmapsVariantsArray[i] = pixmapsVariant.get_child_value(i); + + const pixmapsSizedArray = pixmapsVariantsArray.map((pixmapVariant, index) => ({ + width: pixmapVariant.get_child_value(0).unpack(), + height: pixmapVariant.get_child_value(1).unpack(), + index, + })); + + const sortedIconPixmapArray = pixmapsSizedArray.sort( + ({width: widthA, height: heightA}, {width: widthB, height: heightB}) => { + const areaA = widthA * heightA; + const areaB = widthB * heightB; + + return areaA - areaB; + }); + + // we prefer any pixmap that is equal or bigger than our requested size + const qualifiedIconPixmapArray = sortedIconPixmapArray.filter(({width, height}) => + width >= preferredSize && height >= preferredSize); + + const {width, height, index} = qualifiedIconPixmapArray.length > 0 + ? qualifiedIconPixmapArray[0] : sortedIconPixmapArray.pop(); + + const pixmapVariant = pixmapsVariantsArray[index].get_child_value(2); + const rowStride = width * 4; // hopefully this is correct + + return {pixmapVariant, width, height, rowStride}; +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/prefs.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/prefs.js new file mode 100644 index 0000000..625bc10 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/prefs.js @@ -0,0 +1,326 @@ +// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- + +/* exported init, buildPrefsWidget */ + +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; +import Gtk from 'gi://Gtk'; + +import { + ExtensionPreferences, + gettext as _ +} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +const AppIndicatorPreferences = GObject.registerClass( +class AppIndicatorPreferences extends Gtk.Box { + _init(extension) { + super._init({orientation: Gtk.Orientation.VERTICAL, spacing: 30}); + this._settings = extension.getSettings(); + + let label = null; + let widget = null; + + this.preferences_vbox = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + spacing: 8, + margin_start: 30, + margin_end: 30, + margin_top: 30, + margin_bottom: 30, + }); + this.custom_icons_vbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + + label = new Gtk.Label({ + label: _('Enable Legacy Tray Icons support'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.Switch({halign: Gtk.Align.END}); + + this._settings.bind('legacy-tray-enabled', widget, 'active', + Gio.SettingsBindFlags.DEFAULT); + + this.legacy_tray_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + + this.legacy_tray_hbox.append(label); + this.legacy_tray_hbox.append(widget); + + // Icon opacity + this.opacity_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Opacity (min: 0, max: 255)'), + hexpand: true, + halign: Gtk.Align.START, + }); + + widget = new Gtk.SpinButton({halign: Gtk.Align.END}); + widget.set_sensitive(true); + widget.set_range(0, 255); + widget.set_value(this._settings.get_int('icon-opacity')); + widget.set_increments(1, 2); + widget.connect('value-changed', w => { + this._settings.set_int('icon-opacity', w.get_value_as_int()); + }); + this.opacity_hbox.append(label); + this.opacity_hbox.append(widget); + + // Icon saturation + this.saturation_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Desaturation (min: 0.0, max: 1.0)'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1}); + widget.set_sensitive(true); + widget.set_range(0.0, 1.0); + widget.set_value(this._settings.get_double('icon-saturation')); + widget.set_increments(0.1, 0.2); + widget.connect('value-changed', w => { + this._settings.set_double('icon-saturation', w.get_value()); + }); + this.saturation_hbox.append(label); + this.saturation_hbox.append(widget); + + // Icon brightness + this.brightness_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Brightness (min: -1.0, max: 1.0)'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1}); + widget.set_sensitive(true); + widget.set_range(-1.0, 1.0); + widget.set_value(this._settings.get_double('icon-brightness')); + widget.set_increments(0.1, 0.2); + widget.connect('value-changed', w => { + this._settings.set_double('icon-brightness', w.get_value()); + }); + this.brightness_hbox.append(label); + this.brightness_hbox.append(widget); + + // Icon contrast + this.contrast_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Contrast (min: -1.0, max: 1.0)'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.SpinButton({halign: Gtk.Align.END, digits: 1}); + widget.set_sensitive(true); + widget.set_range(-1.0, 1.0); + widget.set_value(this._settings.get_double('icon-contrast')); + widget.set_increments(0.1, 0.2); + widget.connect('value-changed', w => { + this._settings.set_double('icon-contrast', w.get_value()); + }); + this.contrast_hbox.append(label); + this.contrast_hbox.append(widget); + + // Icon size + this.icon_size_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Icon size (min: 0, max: 96)'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.SpinButton({halign: Gtk.Align.END}); + widget.set_sensitive(true); + widget.set_range(0, 96); + widget.set_value(this._settings.get_int('icon-size')); + widget.set_increments(1, 2); + widget.connect('value-changed', w => { + this._settings.set_int('icon-size', w.get_value_as_int()); + }); + this.icon_size_hbox.append(label); + this.icon_size_hbox.append(widget); + + // Tray position in panel + this.tray_position_hbox = new Gtk.Box({ + orientation: Gtk.Orientation.HORIZONTAL, + spacing: 10, + margin_start: 10, + margin_end: 10, + margin_top: 10, + margin_bottom: 10, + }); + label = new Gtk.Label({ + label: _('Tray horizontal alignment'), + hexpand: true, + halign: Gtk.Align.START, + }); + widget = new Gtk.ComboBoxText(); + widget.append('center', _('Center')); + widget.append('left', _('Left')); + widget.append('right', _('Right')); + this._settings.bind('tray-pos', widget, 'active-id', + Gio.SettingsBindFlags.DEFAULT); + this.tray_position_hbox.append(label); + this.tray_position_hbox.append(widget); + + this.preferences_vbox.append(this.legacy_tray_hbox); + this.preferences_vbox.append(this.opacity_hbox); + this.preferences_vbox.append(this.saturation_hbox); + this.preferences_vbox.append(this.brightness_hbox); + this.preferences_vbox.append(this.contrast_hbox); + this.preferences_vbox.append(this.icon_size_hbox); + this.preferences_vbox.append(this.tray_position_hbox); + + // Custom icons section + + const customListStore = new Gtk.ListStore(); + customListStore.set_column_types([ + GObject.TYPE_STRING, + GObject.TYPE_STRING, + GObject.TYPE_STRING, + ]); + const customInitArray = this._settings.get_value('custom-icons').deep_unpack(); + customInitArray.forEach(pair => { + customListStore.set(customListStore.append(), [0, 1, 2], pair); + }); + customListStore.append(); + + const customTreeView = new Gtk.TreeView({ + model: customListStore, + hexpand: true, + vexpand: true, + }); + const customTitles = [ + _('Indicator ID'), + _('Icon Name'), + _('Attention Icon Name'), + ]; + const indicatorIdColumn = new Gtk.TreeViewColumn({ + title: customTitles[0], + sizing: Gtk.TreeViewColumnSizing.AUTOSIZE, + }); + const customIconColumn = new Gtk.TreeViewColumn({ + title: customTitles[1], + sizing: Gtk.TreeViewColumnSizing.AUTOSIZE, + }); + const customAttentionIconColumn = new Gtk.TreeViewColumn({ + title: customTitles[2], + sizing: Gtk.TreeViewColumnSizing.AUTOSIZE, + }); + + const cellrenderer = new Gtk.CellRendererText({editable: true}); + + indicatorIdColumn.pack_start(cellrenderer, true); + customIconColumn.pack_start(cellrenderer, true); + customAttentionIconColumn.pack_start(cellrenderer, true); + indicatorIdColumn.add_attribute(cellrenderer, 'text', 0); + customIconColumn.add_attribute(cellrenderer, 'text', 1); + customAttentionIconColumn.add_attribute(cellrenderer, 'text', 2); + customTreeView.insert_column(indicatorIdColumn, 0); + customTreeView.insert_column(customIconColumn, 1); + customTreeView.insert_column(customAttentionIconColumn, 2); + customTreeView.set_grid_lines(Gtk.TreeViewGridLines.BOTH); + + this.custom_icons_vbox.append(customTreeView); + + cellrenderer.connect('edited', (w, path, text) => { + this.selection = customTreeView.get_selection(); + const title = customTreeView.get_cursor()[1].get_title(); + const columnIndex = customTitles.indexOf(title); + const selection = this.selection.get_selected(); + const iter = selection.at(2); + const text2 = customListStore.get_value(iter, columnIndex ? 0 : 1); + customListStore.set(iter, [columnIndex], [text]); + const storeLength = customListStore.iter_n_children(null); + const customIconArray = []; + + for (let i = 0; i < storeLength; i++) { + const returnIter = customListStore.iter_nth_child(null, i); + const [success, iterList] = returnIter; + if (!success) + break; + + if (iterList) { + const id = customListStore.get_value(iterList, 0); + const customIcon = customListStore.get_value(iterList, 1); + const customAttentionIcon = customListStore.get_value(iterList, 2); + if (id && customIcon) + customIconArray.push([id, customIcon, customAttentionIcon || '']); + } else { + break; + } + } + this._settings.set_value('custom-icons', new GLib.Variant( + 'a(sss)', customIconArray)); + if (storeLength === 1 && (text || text2)) + customListStore.append(); + + if (storeLength > 1) { + if ((!text && !text2) && (storeLength - 1 > path)) + customListStore.remove(iter); + if ((text || text2) && storeLength - 1 <= path) + customListStore.append(); + } + }); + + this.notebook = new Gtk.Notebook(); + this.notebook.append_page(this.preferences_vbox, + new Gtk.Label({label: _('Preferences')})); + this.notebook.append_page(this.custom_icons_vbox, + new Gtk.Label({label: _('Custom Icons')})); + + this.append(this.notebook); + } +}); + +export default class DockPreferences extends ExtensionPreferences { + getPreferencesWidget() { + return new AppIndicatorPreferences(this); + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js new file mode 100644 index 0000000..2749b03 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/promiseUtils.js @@ -0,0 +1,324 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Meta from 'gi://GdkPixbuf'; + +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +export class CancellablePromise extends Promise { + constructor(executor, cancellable) { + if (!(executor instanceof Function)) + throw TypeError('executor is not a function'); + + if (cancellable && !(cancellable instanceof Gio.Cancellable)) + throw TypeError('cancellable parameter is not a Gio.Cancellable'); + + let rejector; + let resolver; + super((resolve, reject) => { + resolver = resolve; + rejector = reject; + }); + + const {stack: promiseStack} = new Error(); + this._promiseStack = promiseStack; + + this._resolver = (...args) => { + resolver(...args); + this._resolved = true; + this._cleanup(); + }; + this._rejector = (...args) => { + rejector(...args); + this._rejected = true; + this._cleanup(); + }; + + if (!cancellable) { + executor(this._resolver, this._rejector); + return; + } + + this._cancellable = cancellable; + this._cancelled = cancellable.is_cancelled(); + if (this._cancelled) { + this._rejector(new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.CANCELLED, 'Promise cancelled')); + return; + } + + this._cancellationId = cancellable.connect(() => { + const id = this._cancellationId; + this._cancellationId = 0; + GLib.idle_add(GLib.PRIORITY_DEFAULT, () => cancellable.disconnect(id)); + this.cancel(); + }); + + executor(this._resolver, this._rejector); + } + + _cleanup() { + if (this._cancellationId) + this._cancellable.disconnect(this._cancellationId); + } + + get cancellable() { + return this._chainRoot._cancellable || null; + } + + get _chainRoot() { + return this._root ? this._root : this; + } + + then(...args) { + const ret = super.then(...args); + + /* Every time we call then() on this promise we'd get a new + * CancellablePromise however that won't have the properties that the + * root one has set, and then it won't be possible to cancel a promise + * chain from the last one. + * To allow this we keep track of the root promise, make sure that + * the same method on the root object is called during cancellation + * or any destruction method if you want this to work. */ + if (ret instanceof CancellablePromise) + ret._root = this._chainRoot; + + return ret; + } + + resolved() { + return !!this._chainRoot._resolved; + } + + rejected() { + return !!this._chainRoot._rejected; + } + + cancelled() { + return !!this._chainRoot._cancelled; + } + + pending() { + return !this.resolved() && !this.rejected(); + } + + cancel() { + if (this._root) { + this._root.cancel(); + return this; + } + + if (!this.pending()) + return this; + + this._cancelled = true; + const error = new GLib.Error(Gio.IOErrorEnum, + Gio.IOErrorEnum.CANCELLED, 'Promise cancelled'); + error.stack += `## Promise created at:\n${this._promiseStack}`; + this._rejector(error); + + return this; + } +} + +export class SignalConnectionPromise extends CancellablePromise { + constructor(object, signal, cancellable) { + if (arguments.length === 1 && object instanceof Function) { + super(object); + return; + } + + if (!(object.connect instanceof Function)) + throw new TypeError('Not a valid object'); + + if (object instanceof GObject.Object && + !GObject.signal_lookup(signal.split(':')[0], object.constructor.$gtype)) + throw new TypeError(`Signal ${signal} not found on object ${object}`); + + let id; + let destroyId; + super(resolve => { + let connectSignal; + if (object instanceof GObject.Object) + connectSignal = (sig, cb) => GObject.signal_connect(object, sig, cb); + else + connectSignal = (sig, cb) => object.connect(sig, cb); + + id = connectSignal(signal, (_obj, ...args) => { + if (!args.length) + resolve(); + else + resolve(args.length === 1 ? args[0] : args); + }); + + if (signal !== 'destroy' && + (!(object instanceof GObject.Object) || + GObject.signal_lookup('destroy', object.constructor.$gtype))) + destroyId = connectSignal('destroy', () => this.cancel()); + }, cancellable); + + this._object = object; + this._id = id; + this._destroyId = destroyId; + } + + _cleanup() { + if (this._id) { + let disconnectSignal; + + if (this._object instanceof GObject.Object) + disconnectSignal = id => GObject.signal_handler_disconnect(this._object, id); + else + disconnectSignal = id => this._object.disconnect(id); + + disconnectSignal(this._id); + if (this._destroyId) { + disconnectSignal(this._destroyId); + this._destroyId = 0; + } + this._object = null; + this._id = 0; + } + + super._cleanup(); + } + + get object() { + return this._chainRoot._object; + } +} + +export class GSourcePromise extends CancellablePromise { + constructor(gsource, priority, cancellable) { + if (arguments.length === 1 && gsource instanceof Function) { + super(gsource); + return; + } + + if (gsource.constructor.$gtype !== GLib.Source.$gtype) + throw new TypeError(`gsource ${gsource} is not of type GLib.Source`); + + if (priority === undefined) + priority = GLib.PRIORITY_DEFAULT; + else if (!Number.isInteger(priority)) + throw TypeError('Invalid priority'); + + super(resolve => { + gsource.set_priority(priority); + gsource.set_callback(() => { + resolve(); + return GLib.SOURCE_REMOVE; + }); + gsource.attach(null); + }, cancellable); + + this._gsource = gsource; + this._gsource.set_name(`[gnome-shell] ${this.constructor.name} ${ + new Error().stack.split('\n').filter(line => + !line.match(/misc\/promiseUtils\.js/))[0]}`); + + if (this.rejected()) + this._gsource.destroy(); + } + + get gsource() { + return this._chainRoot._gsource; + } + + _cleanup() { + if (this._gsource) { + this._gsource.destroy(); + this._gsource = null; + } + super._cleanup(); + } +} + +export class IdlePromise extends GSourcePromise { + constructor(priority, cancellable) { + if (arguments.length === 1 && priority instanceof Function) { + super(priority); + return; + } + + if (priority === undefined) + priority = GLib.PRIORITY_DEFAULT_IDLE; + + super(GLib.idle_source_new(), priority, cancellable); + } +} + +export class TimeoutPromise extends GSourcePromise { + constructor(interval, priority, cancellable) { + if (arguments.length === 1 && interval instanceof Function) { + super(interval); + return; + } + + if (!Number.isInteger(interval) || interval < 0) + throw TypeError('Invalid interval'); + + super(GLib.timeout_source_new(interval), priority, cancellable); + } +} + +export class TimeoutSecondsPromise extends GSourcePromise { + constructor(interval, priority, cancellable) { + if (arguments.length === 1 && interval instanceof Function) { + super(interval); + return; + } + + if (!Number.isInteger(interval) || interval < 0) + throw TypeError('Invalid interval'); + + super(GLib.timeout_source_new_seconds(interval), priority, cancellable); + } +} + +export class MetaLaterPromise extends CancellablePromise { + constructor(laterType, cancellable) { + if (arguments.length === 1 && laterType instanceof Function) { + super(laterType); + return; + } + + if (laterType && laterType.constructor.$gtype !== Meta.LaterType.$gtype) + throw new TypeError(`laterType ${laterType} is not of type Meta.LaterType`); + else if (!laterType) + laterType = Meta.LaterType.BEFORE_REDRAW; + + let id; + super(resolve => { + id = Meta.later_add(laterType, () => { + this.remove(); + resolve(); + return GLib.SOURCE_REMOVE; + }); + }, cancellable); + + this._id = id; + } + + _cleanup() { + if (this._id) { + Meta.later_remove(this._id); + this._id = 0; + } + super._cleanup(); + } +} + +export function _promisifySignals(proto) { + if (proto.connect_once) + return; + + proto.connect_once = function (signal, cancellable) { + return new SignalConnectionPromise(this, signal, cancellable); + }; +} + +_promisifySignals(GObject.Object.prototype); +_promisifySignals(Signals.EventEmitter.prototype); diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..65205698b60039cf60a82462b73d42df360ec14a GIT binary patch literal 820 zcmYjP&ubJh7|qtMZhwdc5j^y!;tVn~6?#_+VujvXq{n7w!)#!i43o6DEtPt!ClUMu zL=*%)c<4pEDE<>3ym{$y!5?@K>U)_T-N2jo^77?N=F4~YzO1w~UW~_O@Mf)}sm27i zs_E~o@!EBs&%oaVZvh_<9?r22 z)NS~E@H61ri~Z+}r@jFHHTW24>PF4Fk-q{PCr7UsPtEud_z!US=7tKEk6aug5sel1uMM!$i}M}l z?RLTdCn6a+=i)knt$HfFoj`d*-jK!VJ;ix5a9&zU-^V;dLu8>Lvd|FO|AxpW8zK|J z)w!hKsia58G}&s#z3_XXFHnjw*e7jZ7GQs{hj_1$ryKs%DCgiz4CJZLVU0`?dJy*= Ys6yt$B$P=R*&V1}*whkHHy&^Q3q9SVH~;_u literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml new file mode 100644 index 0000000..75bba7c --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/schemas/org.gnome.shell.extensions.appindicator.gschema.xml @@ -0,0 +1,49 @@ + + + + true + Enable legacy tray icons support + + + 0.0 + Saturation + + + 0.0 + Brightness + + + 0.0 + Contrast + + + 240 + Opacity + + + 0 + Icon size + Icon size in pixel + + + 12 + Icon spacing + Icon spacing within the tray + + + "right" + Position in tray + Set where the Icon tray should appear in Gnome tray + + + 1 + Order in tray + Set where the Icon tray should appear among other trays + + + [] + Custom icons + Replace any icons with custom icons from themes + + + diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js new file mode 100644 index 0000000..6d943b9 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/settingsManager.js @@ -0,0 +1,55 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +let settingsManager; + +export class SettingsManager { + static initialize(extension) { + SettingsManager._settingsManager = new SettingsManager(extension); + } + + static destroy() { + SettingsManager._settingsManager.destroy(); + SettingsManager._settingsManager = null; + } + + static getDefault() { + return this._settingsManager; + } + + get gsettings() { + return this._gsettings; + } + + constructor(extension) { + if (settingsManager) + throw new Error('SettingsManager is already constructed'); + + this._gsettings = extension.getSettings(); + } + + destroy() { + this._gsettings = null; + } +} + +export function getDefault() { + return SettingsManager.getDefault(); +} + +export function getDefaultGSettings() { + return SettingsManager.getDefault().gsettings; +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js new file mode 100644 index 0000000..915c860 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/statusNotifierWatcher.js @@ -0,0 +1,287 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +import * as AppIndicator from './appIndicator.js'; +import * as IndicatorStatusIcon from './indicatorStatusIcon.js'; +import * as Interfaces from './interfaces.js'; +import * as PromiseUtils from './promiseUtils.js'; +import * as Util from './util.js'; +import * as DBusMenu from './dbusMenu.js'; + +import {DBusProxy} from './dbusProxy.js'; + + +// TODO: replace with org.freedesktop and /org/freedesktop when approved +const KDE_PREFIX = 'org.kde'; + +export const WATCHER_BUS_NAME = `${KDE_PREFIX}.StatusNotifierWatcher`; +const WATCHER_OBJECT = '/StatusNotifierWatcher'; + +const DEFAULT_ITEM_OBJECT_PATH = '/StatusNotifierItem'; + +/* + * The StatusNotifierWatcher class implements the StatusNotifierWatcher dbus object + */ +export class StatusNotifierWatcher { + constructor(watchDog) { + this._watchDog = watchDog; + this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(Interfaces.StatusNotifierWatcher, this); + try { + this._dbusImpl.export(Gio.DBus.session, WATCHER_OBJECT); + } catch (e) { + Util.Logger.warn(`Failed to export ${WATCHER_OBJECT}`); + logError(e); + } + this._cancellable = new Gio.Cancellable(); + this._everAcquiredName = false; + this._ownName = Gio.DBus.session.own_name(WATCHER_BUS_NAME, + Gio.BusNameOwnerFlags.NONE, + this._acquiredName.bind(this), + this._lostName.bind(this)); + this._items = new Map(); + + try { + this._dbusImpl.emit_signal('StatusNotifierHostRegistered', null); + } catch (e) { + Util.Logger.warn(`Failed to notify registered host ${WATCHER_OBJECT}`); + } + + this._seekStatusNotifierItems().catch(e => { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e, 'Looking for StatusNotifierItem\'s'); + }); + } + + _acquiredName() { + this._everAcquiredName = true; + this._watchDog.nameAcquired = true; + } + + _lostName() { + if (this._everAcquiredName) + Util.Logger.debug(`Lost name${WATCHER_BUS_NAME}`); + else + Util.Logger.warn(`Failed to acquire ${WATCHER_BUS_NAME}`); + this._watchDog.nameAcquired = false; + } + + async _registerItem(service, busName, objPath) { + const id = Util.indicatorId(service, busName, objPath); + + if (this._items.has(id)) { + Util.Logger.warn(`Item ${id} is already registered`); + return; + } + + Util.Logger.debug(`Registering StatusNotifierItem ${id}`); + + try { + const indicator = new AppIndicator.AppIndicator(service, busName, objPath); + this._items.set(id, indicator); + indicator.connect('destroy', () => this._onIndicatorDestroyed(indicator)); + + indicator.connect('name-owner-changed', async () => { + if (!indicator.hasNameOwner) { + try { + await new PromiseUtils.TimeoutPromise(500, + GLib.PRIORITY_DEFAULT, this._cancellable); + if (this._items.has(id) && !indicator.hasNameOwner) + indicator.destroy(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + }); + + // if the desktop is not ready delay the icon creation and signal emissions + await Util.waitForStartupCompletion(indicator.cancellable); + const statusIcon = new IndicatorStatusIcon.IndicatorStatusIcon(indicator); + IndicatorStatusIcon.addIconToPanel(statusIcon); + + this._dbusImpl.emit_signal('StatusNotifierItemRegistered', + GLib.Variant.new('(s)', [indicator.uniqueId])); + this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', + GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + throw e; + } + } + + async _ensureItemRegistered(service, busName, objPath) { + const id = Util.indicatorId(service, busName, objPath); + const item = this._items.get(id); + + if (item) { + // delete the old one and add the new indicator + Util.Logger.debug(`Attempting to re-register ${id}; resetting instead`); + item.reset(); + return; + } + + await this._registerItem(service, busName, objPath); + } + + async _seekStatusNotifierItems() { + // Some indicators (*coff*, dropbox, *coff*) do not re-register again + // when the plugin is enabled/disabled, thus we need to manually look + // for the objects in the session bus that implements the + // StatusNotifierItem interface... However let's do it after a low + // priority idle, so that it won't affect startup. + const cancellable = this._cancellable; + const bus = Gio.DBus.session; + const uniqueNames = await Util.getBusNames(bus, cancellable); + const introspectName = async name => { + const nodes = Util.introspectBusObject(bus, name, cancellable, + ['org.kde.StatusNotifierItem']); + const services = [...uniqueNames.get(name)]; + + for await (const node of nodes) { + const {path} = node; + const ids = services.map(s => Util.indicatorId(s, name, path)); + if (ids.every(id => !this._items.has(id))) { + const service = services.find(s => + s && s.startsWith('org.kde.StatusNotifierItem')) || services[0]; + const id = Util.indicatorId( + path === DEFAULT_ITEM_OBJECT_PATH ? service : null, + name, path); + Util.Logger.warn(`Using Brute-force mode for StatusNotifierItem ${id}`); + this._registerItem(service, name, path); + } + } + }; + await Promise.allSettled([...uniqueNames.keys()].map(n => introspectName(n))); + } + + async RegisterStatusNotifierItemAsync(params, invocation) { + // it would be too easy if all application behaved the same + // instead, ayatana patched gnome apps to send a path + // while kde apps send a bus name + const [service] = params; + let busName, objPath; + + if (service.charAt(0) === '/') { // looks like a path + busName = invocation.get_sender(); + objPath = service; + } else if (service.match(Util.BUS_ADDRESS_REGEX)) { + try { + busName = await Util.getUniqueBusName(invocation.get_connection(), + service, this._cancellable); + } catch (e) { + logError(e); + } + objPath = DEFAULT_ITEM_OBJECT_PATH; + } + + if (!busName || !objPath) { + const error = `Impossible to register an indicator for parameters '${ + service.toString()}'`; + Util.Logger.warn(error); + + invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', + error); + return; + } + + try { + await this._ensureItemRegistered(service, busName, objPath); + invocation.return_value(null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + invocation.return_dbus_error('org.gnome.gjs.JSError.ValueError', + e.message); + } + } + + _onIndicatorDestroyed(indicator) { + const {uniqueId} = indicator; + this._items.delete(uniqueId); + + try { + this._dbusImpl.emit_signal('StatusNotifierItemUnregistered', + GLib.Variant.new('(s)', [uniqueId])); + this._dbusImpl.emit_property_changed('RegisteredStatusNotifierItems', + GLib.Variant.new('as', this.RegisteredStatusNotifierItems)); + } catch (e) { + Util.Logger.warn(`Failed to emit signals: ${e}`); + } + } + + RegisterStatusNotifierHostAsync(_service, invocation) { + invocation.return_error_literal( + Gio.DBusError, + Gio.DBusError.NOT_SUPPORTED, + 'Registering additional notification hosts is not supported'); + } + + IsNotificationHostRegistered() { + return true; + } + + get RegisteredStatusNotifierItems() { + return Array.from(this._items.values()).map(i => i.uniqueId); + } + + get IsStatusNotifierHostRegistered() { + return true; + } + + get ProtocolVersion() { + return 0; + } + + destroy() { + if (this._isDestroyed) + return; + + // this doesn't do any sync operation and doesn't allow us to hook up + // the event of being finished which results in our unholy debounce hack + // (see extension.js) + this._items.forEach(indicator => indicator.destroy()); + this._cancellable.cancel(); + + try { + this._dbusImpl.emit_signal('StatusNotifierHostUnregistered', null); + } catch (e) { + Util.Logger.warn(`Failed to emit uinregistered signal: ${e}`); + } + + Gio.DBus.session.unown_name(this._ownName); + + try { + this._dbusImpl.unexport(); + } catch (e) { + Util.Logger.warn(`Failed to unexport watcher object: ${e}`); + } + + DBusMenu.DBusClient.destroy(); + AppIndicator.AppIndicatorProxy.destroy(); + DBusProxy.destroy(); + Util.destroyDefaultTheme(); + + this._dbusImpl.run_dispose(); + delete this._dbusImpl; + + delete this._items; + this._isDestroyed = true; + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js new file mode 100644 index 0000000..cdc2a8e --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/trayIconsManager.js @@ -0,0 +1,104 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Shell from 'gi://Shell'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +import * as IndicatorStatusIcon from './indicatorStatusIcon.js'; +import * as Util from './util.js'; +import * as SettingsManager from './settingsManager.js'; + +let trayIconsManager; + +export class TrayIconsManager extends Signals.EventEmitter { + static initialize() { + if (!trayIconsManager) + trayIconsManager = new TrayIconsManager(); + return trayIconsManager; + } + + static destroy() { + trayIconsManager.destroy(); + } + + constructor() { + super(); + + if (trayIconsManager) + throw new Error('TrayIconsManager is already constructed'); + + this._changedId = SettingsManager.getDefaultGSettings().connect( + 'changed::legacy-tray-enabled', () => this._toggle()); + + this._toggle(); + } + + _toggle() { + if (SettingsManager.getDefaultGSettings().get_boolean('legacy-tray-enabled')) + this._enable(); + else + this._disable(); + } + + _enable() { + if (this._tray) + return; + + this._tray = new Shell.TrayManager(); + Util.connectSmart(this._tray, 'tray-icon-added', this, this.onTrayIconAdded); + Util.connectSmart(this._tray, 'tray-icon-removed', this, this.onTrayIconRemoved); + + this._tray.manage_screen(Main.panel); + } + + _disable() { + if (!this._tray) + return; + + IndicatorStatusIcon.getTrayIcons().forEach(i => i.destroy()); + if (this._tray.unmanage_screen) { + this._tray.unmanage_screen(); + this._tray = null; + } else { + // FIXME: This is very ugly, but it's needed by old shell versions + this._tray = null; + imports.system.gc(); // force finalizing tray to unmanage screen + } + } + + onTrayIconAdded(_tray, icon) { + const trayIcon = new IndicatorStatusIcon.IndicatorStatusTrayIcon(icon); + IndicatorStatusIcon.addIconToPanel(trayIcon); + } + + onTrayIconRemoved(_tray, icon) { + try { + const [trayIcon] = IndicatorStatusIcon.getTrayIcons().filter(i => i.icon === icon); + trayIcon.destroy(); + } catch (e) { + Util.Logger.warning(`No icon container found for ${icon.title} (${icon})`); + } + } + + destroy() { + this.emit('destroy'); + SettingsManager.getDefaultGSettings().disconnect(this._changedId); + this._disable(); + trayIconsManager = null; + } +} diff --git a/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/util.js b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/util.js new file mode 100644 index 0000000..d2cb352 --- /dev/null +++ b/gnome/.local/share/extensions/appindicatorsupport@rgcjonas.gmail.com/util.js @@ -0,0 +1,434 @@ +// This file is part of the AppIndicator/KStatusNotifierItem GNOME Shell extension +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; + +const ByteArray = imports.byteArray; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Config from 'resource:///org/gnome/shell/misc/config.js'; +import * as Signals from 'resource:///org/gnome/shell/misc/signals.js'; + +import {BaseStatusIcon} from './indicatorStatusIcon.js'; + +export const BUS_ADDRESS_REGEX = /([a-zA-Z0-9._-]+\.[a-zA-Z0-9.-]+)|(:[0-9]+\.[0-9]+)$/; + +Gio._promisify(Gio.DBusConnection.prototype, 'call'); +Gio._promisify(Gio._LocalFilePrototype, 'read'); +Gio._promisify(Gio.InputStream.prototype, 'read_bytes_async'); + +export function indicatorId(service, busName, objectPath) { + if (service !== busName && service?.match(BUS_ADDRESS_REGEX)) + return service; + + return `${busName}@${objectPath}`; +} + +export async function getUniqueBusName(bus, name, cancellable) { + if (name[0] === ':') + return name; + + if (!bus) + bus = Gio.DBus.session; + + const variantName = new GLib.Variant('(s)', [name]); + const [unique] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', + 'GetNameOwner', variantName, new GLib.VariantType('(s)'), + Gio.DBusCallFlags.NONE, -1, cancellable)).deep_unpack(); + + return unique; +} + +export async function getBusNames(bus, cancellable) { + if (!bus) + bus = Gio.DBus.session; + + const [names] = (await bus.call('org.freedesktop.DBus', '/', 'org.freedesktop.DBus', + 'ListNames', null, new GLib.VariantType('(as)'), Gio.DBusCallFlags.NONE, + -1, cancellable)).deep_unpack(); + + const uniqueNames = new Map(); + const requests = names.map(name => getUniqueBusName(bus, name, cancellable)); + const results = await Promise.allSettled(requests); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === 'fulfilled') { + let namesForBus = uniqueNames.get(result.value); + if (!namesForBus) { + namesForBus = new Set(); + uniqueNames.set(result.value, namesForBus); + } + namesForBus.add(result.value !== names[i] ? names[i] : null); + } else if (!result.reason.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + Logger.debug(`Impossible to get the unique name of ${names[i]}: ${result.reason}`); + } + } + + return uniqueNames; +} + +async function getProcessId(connectionName, cancellable = null, bus = Gio.DBus.session) { + const res = await bus.call('org.freedesktop.DBus', '/', + 'org.freedesktop.DBus', 'GetConnectionUnixProcessID', + new GLib.Variant('(s)', [connectionName]), + new GLib.VariantType('(u)'), + Gio.DBusCallFlags.NONE, + -1, + cancellable); + const [pid] = res.deepUnpack(); + return pid; +} + +export async function getProcessName(connectionName, cancellable = null, + priority = GLib.PRIORITY_DEFAULT, bus = Gio.DBus.session) { + const pid = await getProcessId(connectionName, cancellable, bus); + const cmdFile = Gio.File.new_for_path(`/proc/${pid}/cmdline`); + const inputStream = await cmdFile.read_async(priority, cancellable); + const bytes = await inputStream.read_bytes_async(2048, priority, cancellable); + return ByteArray.toString(bytes.toArray().map(v => !v ? 0x20 : v)); +} + +export async function* introspectBusObject(bus, name, cancellable, + interfaces = undefined, path = undefined) { + if (!path) + path = '/'; + + const [introspection] = (await bus.call(name, path, 'org.freedesktop.DBus.Introspectable', + 'Introspect', null, new GLib.VariantType('(s)'), Gio.DBusCallFlags.NONE, + 5000, cancellable)).deep_unpack(); + + const nodeInfo = Gio.DBusNodeInfo.new_for_xml(introspection); + + if (!interfaces || dbusNodeImplementsInterfaces(nodeInfo, interfaces)) + yield {nodeInfo, path}; + + if (path === '/') + path = ''; + + for (const subNodeInfo of nodeInfo.nodes) { + const subPath = `${path}/${subNodeInfo.path}`; + yield* introspectBusObject(bus, name, cancellable, interfaces, subPath); + } +} + +function dbusNodeImplementsInterfaces(nodeInfo, interfaces) { + if (!(nodeInfo instanceof Gio.DBusNodeInfo) || !Array.isArray(interfaces)) + return false; + + return interfaces.some(iface => nodeInfo.lookup_interface(iface)); +} + +export class NameWatcher extends Signals.EventEmitter { + constructor(name) { + super(); + + this._watcherId = Gio.DBus.session.watch_name(name, + Gio.BusNameWatcherFlags.NONE, () => { + this._nameOnBus = true; + Logger.debug(`Name ${name} appeared`); + this.emit('changed'); + this.emit('appeared'); + }, () => { + this._nameOnBus = false; + Logger.debug(`Name ${name} vanished`); + this.emit('changed'); + this.emit('vanished'); + }); + } + + destroy() { + this.emit('destroy'); + + Gio.DBus.session.unwatch_name(this._watcherId); + delete this._watcherId; + } + + get nameOnBus() { + return !!this._nameOnBus; + } +} + +function connectSmart3A(src, signal, handler) { + const id = src.connect(signal, handler); + let destroyId = 0; + + if (src.connect && (!(src instanceof GObject.Object) || GObject.signal_lookup('destroy', src))) { + destroyId = src.connect('destroy', () => { + src.disconnect(id); + src.disconnect(destroyId); + }); + } + + return [id, destroyId]; +} + +function connectSmart4A(src, signal, target, method) { + if (typeof method !== 'function') + throw new TypeError('Unsupported function'); + + method = method.bind(target); + const signalId = src.connect(signal, method); + const onDestroy = () => { + src.disconnect(signalId); + if (srcDestroyId) + src.disconnect(srcDestroyId); + if (tgtDestroyId) + target.disconnect(tgtDestroyId); + }; + + // GObject classes might or might not have a destroy signal + // JS Classes will not complain when connecting to non-existent signals + const srcDestroyId = src.connect && (!(src instanceof GObject.Object) || + GObject.signal_lookup('destroy', src)) ? src.connect('destroy', onDestroy) : 0; + const tgtDestroyId = target.connect && (!(target instanceof GObject.Object) || + GObject.signal_lookup('destroy', target)) ? target.connect('destroy', onDestroy) : 0; + + return [signalId, srcDestroyId, tgtDestroyId]; +} + +// eslint-disable-next-line valid-jsdoc +/** + * Connect signals to slots, and remove the connection when either source or + * target are destroyed + * + * Usage: + * Util.connectSmart(srcOb, 'signal', tgtObj, 'handler') + * or + * Util.connectSmart(srcOb, 'signal', () => { ... }) + */ +export function connectSmart(...args) { + if (arguments.length === 4) + return connectSmart4A(...args); + else + return connectSmart3A(...args); +} + +function disconnectSmart3A(src, signalIds) { + const [id, destroyId] = signalIds; + src.disconnect(id); + + if (destroyId) + src.disconnect(destroyId); +} + +function disconnectSmart4A(src, tgt, signalIds) { + const [signalId, srcDestroyId, tgtDestroyId] = signalIds; + + disconnectSmart3A(src, [signalId, srcDestroyId]); + + if (tgtDestroyId) + tgt.disconnect(tgtDestroyId); +} + +export function disconnectSmart(...args) { + if (arguments.length === 2) + return disconnectSmart3A(...args); + else if (arguments.length === 3) + return disconnectSmart4A(...args); + + throw new TypeError('Unexpected number of arguments'); +} + +let _defaultTheme; +export function getDefaultTheme() { + if (_defaultTheme) + return _defaultTheme; + + _defaultTheme = new St.IconTheme(); + return _defaultTheme; +} + +export function destroyDefaultTheme() { + _defaultTheme = null; +} + +// eslint-disable-next-line valid-jsdoc +/** + * Helper function to wait for the system startup to be completed. + * Adding widgets before the desktop is ready to accept them can result in errors. + */ +export async function waitForStartupCompletion(cancellable) { + if (Main.layoutManager._startingUp) + await Main.layoutManager.connect_once('startup-complete', cancellable); +} + +/** + * Helper class for logging stuff + */ +export class Logger { + static _logStructured(logLevel, message, extraFields = {}) { + if (!Object.values(GLib.LogLevelFlags).includes(logLevel)) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, + 'logLevel is not a valid GLib.LogLevelFlags'); + return; + } + + if (!Logger._levels.includes(logLevel)) + return; + + let fields = { + 'SYSLOG_IDENTIFIER': this.uuid, + 'MESSAGE': `${message}`, + }; + + let thisFile = null; + const {stack} = new Error(); + for (let stackLine of stack.split('\n')) { + stackLine = stackLine.replace('resource:///org/gnome/Shell/', ''); + const [code, line] = stackLine.split(':'); + const [func, file] = code.split(/@(.+)/); + + if (!thisFile || thisFile === file) { + thisFile = file; + continue; + } + + fields = Object.assign(fields, { + 'CODE_FILE': file || '', + 'CODE_LINE': line || '', + 'CODE_FUNC': func || '', + }); + + break; + } + + GLib.log_structured(Logger._domain, logLevel, Object.assign(fields, extraFields)); + } + + static init(extension) { + if (Logger._domain) + return; + + const allLevels = Object.values(GLib.LogLevelFlags); + const domains = GLib.getenv('G_MESSAGES_DEBUG'); + const {name: domain} = extension.metadata; + this.uuid = extension.metadata.uuid; + Logger._domain = domain.replaceAll(' ', '-'); + + if (domains === 'all' || (domains && domains.split(' ').includes(Logger._domain))) { + Logger._levels = allLevels; + } else { + Logger._levels = allLevels.filter( + l => l <= GLib.LogLevelFlags.LEVEL_WARNING); + } + } + + static debug(message) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_DEBUG, message); + } + + static message(message) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_MESSAGE, message); + } + + static warn(message) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_WARNING, message); + } + + static error(message) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_ERROR, message); + } + + static critical(message) { + Logger._logStructured(GLib.LogLevelFlags.LEVEL_CRITICAL, message); + } +} + +export function versionCheck(required) { + const current = Config.PACKAGE_VERSION; + const currentArray = current.split('.'); + const [major] = currentArray; + return major >= required; +} + +export function tryCleanupOldIndicators() { + const indicatorType = BaseStatusIcon; + const indicators = Object.values(Main.panel.statusArea).filter(i => i instanceof indicatorType); + + try { + const panelBoxes = [ + Main.panel._leftBox, Main.panel._centerBox, Main.panel._rightBox, + ]; + + panelBoxes.forEach(box => + indicators.push(...box.get_children().filter(i => i instanceof indicatorType))); + } catch (e) { + logError(e); + } + + new Set(indicators).forEach(i => i.destroy()); +} + +export const CancellableChild = GObject.registerClass({ + Properties: { + 'parent': GObject.ParamSpec.object( + 'parent', 'parent', 'parent', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, + Gio.Cancellable.$gtype), + }, +}, +class CancellableChild extends Gio.Cancellable { + _init(parent) { + if (parent && !(parent instanceof Gio.Cancellable)) + throw TypeError('Not a valid cancellable'); + + super._init({parent}); + + if (parent) { + if (parent.is_cancelled()) { + this.cancel(); + return; + } + + this._connectToParent(); + } + } + + _connectToParent() { + this._connectId = this.parent.connect(() => { + this._realCancel(); + + if (this._disconnectIdle) + return; + + this._disconnectIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { + delete this._disconnectIdle; + this._disconnectFromParent(); + return GLib.SOURCE_REMOVE; + }); + }); + } + + _disconnectFromParent() { + if (this._connectId && !this._disconnectIdle) { + this.parent.disconnect(this._connectId); + delete this._connectId; + } + } + + _realCancel() { + Gio.Cancellable.prototype.cancel.call(this); + } + + cancel() { + this._disconnectFromParent(); + this._realCancel(); + } +}); diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/autoActivities.js b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/autoActivities.js new file mode 100644 index 0000000..505b8ba --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/autoActivities.js @@ -0,0 +1,228 @@ +/**************************************************************************** + ** Auto Activities - Show activities overlay when there are no windows. + ** Copyright (C) 2022-2023 Cleo Menezes Jr., 2021 jan Sena + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +"use strict"; + +import * as Main from "resource:///org/gnome/shell/ui/main.js"; +import GLib from "gi://GLib"; +import GObject from "gi://GObject"; +import St from "gi://St"; +import Meta from "gi://Meta"; + +let windowCheckingDelay; +const ignoredWindowTypes = [ + Meta.WindowType.DROPDOWN_MENU, + Meta.WindowType.NOTIFICATION, + Meta.WindowType.POPUP_MENU, + Meta.WindowType.SPLASHSCREEN, +]; + +export const AutoActivitiesManager = GObject.registerClass( + class ActivitiesManager extends St.Bin { + _init({ remoteModel, monitorIndex, settings }) { + this._remoteModel = remoteModel; + this._monitorIndex = monitorIndex; + this._windowRemovedEvents = []; + this._windowAddedEvents = []; + this._settings = settings; + + for (let i = 0; i < global.workspace_manager.n_workspaces; i++) { + this._onWorkspaceAdded(global.workspace_manager, i); + } + + this._workspaceAddedEvent = global.workspace_manager.connect( + "workspace-added", + this._onWorkspaceAdded.bind(this), + ); + this._workspaceRemovedEvent = global.workspace_manager.connect( + "workspace-removed", + this._onWorkspaceRemoved.bind(this), + ); + this._workspaceSwitchedEvent = global.workspace_manager.connect( + "workspace-switched", + this._onWorkspaceSwitched.bind(this), + ); + this._workspacesReorderedEvent = global.workspace_manager.connect( + "workspaces-reordered", + this._onWorkspacesReordered.bind(this), + ); + this._minimizedEvent = global.window_manager.connect( + "minimize", + this._onWindowMinimized.bind(this), + ); + } + + _onWorkspaceAdded(_sender, workspaceIndex) { + let windowRemovedEvent = global.workspace_manager + .get_workspace_by_index(workspaceIndex) + .connect("window-removed", this._onWindowRemoved.bind(this)); + this._windowRemovedEvents.push(windowRemovedEvent); + let windowAddedEvent = global.workspace_manager + .get_workspace_by_index(workspaceIndex) + .connect("window-added", this._onWindowAdded.bind(this)); + this._windowAddedEvents.push(windowAddedEvent); + } + + _onWorkspaceRemoved(_sender, workspaceIndex) { + if (workspaceIndex < this._windowRemovedEvents.length) { + this._windowRemovedEvents.splice(workspaceIndex); + this._windowAddedEvents.splice(workspaceIndex); + } + } + + _onWorkspaceSwitched( + _sender, + _oldWorkspaceIndex, + _newWorkspaceIndex, + _direction, + ) { + if (!this._settings.get_boolean("skip-last-workspace")) { + this._checkAndShowOverview(); + } + } + + _onWorkspacesReordered(_sender) { + let firstWorkspaceIndex = -1; + let secondWorkspaceIndex = -1; + for (let i = 0; i < this._windowRemovedEvents.length; i++) { + if ( + GObject.signal_handler_is_connected( + global.workspace_manager.get_workspace_by_index(i), + this._windowRemovedEvents[i], + ) + ) { + if (firstWorkspaceIndex < 0) firstWorkspaceIndex = i; + else secondWorkspaceIndex = i; + } + } + + let tempFirstWorkspaceRemovedEvent = + this._windowRemovedEvents[firstWorkspaceIndex]; + this._windowRemovedEvents[firstWorkspaceIndex] = + this._windowRemovedEvents[secondWorkspaceIndex]; + this._windowRemovedEvents[secondWorkspaceIndex] = + tempFirstWorkspaceRemovedEvent; + + let tempFirstWorkspaceAddedEvent = + this._windowAddedEvents[firstWorkspaceIndex]; + this._windowAddedEvents[firstWorkspaceIndex] = + this._windowAddedEvents[secondWorkspaceIndex]; + this._windowAddedEvents[secondWorkspaceIndex] = + tempFirstWorkspaceAddedEvent; + } + + _onWindowMinimized(_sender, actor) { + if ( + this._settings.get_boolean("detect-minimized") && + !ignoredWindowTypes.includes(actor.meta_window.get_window_type()) + ) { + this._checkAndShowOverview(); + } + } + + _onWindowRemoved(_sender, removedWindow) { + if (!ignoredWindowTypes.includes(removedWindow.get_window_type())) { + this._checkAndShowOverview(); + } + } + + _onWindowAdded(_sender, addedWindow) { + if ( + !ignoredWindowTypes.includes(addedWindow.get_window_type()) && + this._settings.get_boolean("hide-on-new-window") + ) { + if (Main.overview.visible) Main.overview.hide(); + } + } + + _checkAndShowOverview() { + let delay = 0; + let delaySetting = this._settings.get_int("window-checking-delay"); + if (!isNaN(delaySetting) && delaySetting > 0) delay = delaySetting; + windowCheckingDelay = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + delay, + () => { + let windows = global.get_window_actors(); + if (this._settings.get_boolean("isolate-workspaces")) { + windows = windows.filter( + (window) => + window.meta_window.get_workspace().index() === + global.workspace_manager.get_active_workspace().index(), + ); + } + if (this._settings.get_boolean("isolate-monitors")) { + windows = windows.filter( + (window) => + window.meta_window.get_monitor() === this._monitorIndex, + ); + } + if (this._settings.get_boolean("skip-taskbar")) { + windows = windows.filter( + (window) => !window.meta_window.skip_taskbar, + ); + } + if (this._settings.get_boolean("detect-minimized")) { + windows = windows.filter((window) => !window.meta_window.minimized); + } + + if (windows.length < 1) { + if (this._settings.get_boolean("show-apps")) { + Main.overview + .showApps(); + } else Main.overview.show(); + } + }, + ); + } + + destroy() { + global.workspace_manager.disconnect(this._workspaceAddedEvent); + global.workspace_manager.disconnect(this._workspaceRemovedEvent); + global.workspace_manager.disconnect(this._workspaceSwitchedEvent); + global.workspace_manager.disconnect(this._workspacesReorderedEvent); + global.window_manager.disconnect(this._minimizedEvent); + + for (let i = 0; i < this._windowRemovedEvents.length; i++) { + if ( + GObject.signal_handler_is_connected( + global.workspace_manager.get_workspace_by_index(i), + this._windowRemovedEvents[i], + ) + ) { + global.workspace_manager + .get_workspace_by_index(i) + .disconnect(this._windowRemovedEvents[i]); + } + } + + for (let i = 0; i < this._windowAddedEvents.length; i++) { + if ( + GObject.signal_handler_is_connected( + global.workspace_manager.get_workspace_by_index(i), + this._windowAddedEvents[i], + ) + ) { + global.workspace_manager + .get_workspace_by_index(i) + .disconnect(this._windowAddedEvents[i]); + } + } + GLib.Source.remove(windowCheckingDelay); + } + }, +); diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/extension.js b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/extension.js new file mode 100644 index 0000000..638004a --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/extension.js @@ -0,0 +1,36 @@ +/**************************************************************************** + ** Auto Activities - Show activities overlay when there are no windows. + ** Copyright (C) 2022-2023 Cleo Menezes Jr., 2021 jan Sena + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +"use strict"; + +import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; +import { AutoActivitiesManager } from "./autoActivities.js"; + +export default class AutoActivities extends Extension { + enable() { + this._settings = this.getSettings(); + this._autoActivities = new AutoActivitiesManager({ + settings: this._settings, + }); + } + + disable() { + this._autoActivities.destroy(); + this._autoActivities = null; + this._settings = null; + } +} diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/metadata.json b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/metadata.json new file mode 100644 index 0000000..c57a2f7 --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/metadata.json @@ -0,0 +1,19 @@ +{ + "_generated": "Generated by SweetTooth, do not edit", + "description": "Show activities overview when there are no windows, or hide it when there are new windows.\n\nThis extension is a fork of the Auto Activities extension, its former owner transferred its repository to me.", + "donations": { + "github": "CleoMenezesJr", + "kofi": "cleomenezesjr", + "paypal": "CleoMenezesJr" + }, + "gettext-domain": "auto-activities", + "name": "Auto Activities", + "original-author": "mi-jan-sena@proton.me", + "settings-schema": "org.gnome.shell.extensions.auto-activities", + "shell-version": [ + "45" + ], + "url": "https://github.com/CleoMenezesJr/auto-activities", + "uuid": "auto-activities@CleoMenezesJr.github.io", + "version": 12 +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.js b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.js new file mode 100644 index 0000000..38934e5 --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.js @@ -0,0 +1,119 @@ +/**************************************************************************** + ** Auto Activities - Show activities overlay when there are no windows. + ** Copyright (C) 2021 jan Sena and Cleo Menezes Jr. + ** + ** This program is free software: you can redistribute it and/or modify + ** it under the terms of the GNU General Public License as published by + ** the Free Software Foundation, either version 3 of the License, or + ** (at your option) any later version. + ** + ** This program is distributed in the hope that it will be useful, + ** but WITHOUT ANY WARRANTY; without even the implied warranty of + ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + ** GNU General Public License for more details. + ** + ** You should have received a copy of the GNU General Public License + ** along with this program. If not, see . + ****************************************************************************/ +"use strict"; + +import Gtk from "gi://Gtk"; +import Gio from "gi://Gio"; +import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; + +export default class MyExtensionPreferences extends ExtensionPreferences { + fillPreferencesWindow(window) { + window._settings = this.getSettings(); + window.set_default_size(360, 724); + + const builder = new Gtk.Builder(); + builder.add_from_file(`${this.path}/prefs.ui`); + + const isolateWorkspaces = builder.get_object("IsolateWorkspacesSwitch"); + isolateWorkspaces.set_active( + window._settings.get_boolean("isolate-workspaces"), + ); + window._settings.bind( + "isolate-workspaces", + isolateWorkspaces, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const isolateMonitors = builder.get_object("IsolateMonitorsSwitch"); + isolateMonitors.set_active( + window._settings.get_boolean("isolate-monitors"), + ); + window._settings.bind( + "isolate-monitors", + isolateMonitors, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const skipTaskbar = builder.get_object("SkipTaskbarSwitch"); + skipTaskbar.set_active(window._settings.get_boolean("skip-taskbar")); + window._settings.bind( + "skip-taskbar", + skipTaskbar, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const skipLastWorkspace = builder.get_object("SkipLastWorkspaceSwitch"); + skipLastWorkspace.set_active( + window._settings.get_boolean("skip-last-workspace"), + ); + window._settings.bind( + "skip-last-workspace", + skipLastWorkspace, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const detectMinimized = builder.get_object("MinimizedSwitch"); + detectMinimized.set_active( + window._settings.get_boolean("detect-minimized"), + ); + window._settings.bind( + "detect-minimized", + detectMinimized, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const hideOnNewWindow = builder.get_object("HideOnNewWindowSwitch"); + hideOnNewWindow.set_active( + window._settings.get_boolean("hide-on-new-window"), + ); + window._settings.bind( + "hide-on-new-window", + hideOnNewWindow, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const checkingDelay = builder.get_object("CheckingDelayEntry"); + checkingDelay.set_value( + window._settings.get_int("window-checking-delay"), + ); + window._settings.bind( + "window-checking-delay", + checkingDelay, + "value", + Gio.SettingsBindFlags.DEFAULT, + ); + + const showApps = builder.get_object("ShowAppsSwitch"); + showApps.set_active(window._settings.get_boolean("show-apps")); + window._settings.bind( + "show-apps", + showApps, + "active", + Gio.SettingsBindFlags.DEFAULT, + ); + + const page = builder.get_object("MainWidget"); + window.add(page); + } +} diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.ui b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.ui new file mode 100644 index 0000000..991dfcf --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/prefs.ui @@ -0,0 +1,124 @@ + + + + Preferences + + + General + + + Detect Hidden Windows + MinimizedSwitch + + + center + True + + + + + + + Always Hide on New Window + HideOnNewWindowSwitch + + + center + True + + + + + + + Window Checking Delay + milliseconds + + + 0 + 10000 + 0 + 10 + 10 + + + + + + + + + + + Show App Grid Instead of Overview + ShowAppsSwitch + + + center + True + + + + + + + + + Isolate + + + Workspaces + IsolateWorkspacesSwitch + + + center + True + + + + + + + Monitors + IsolateMonitorsSwitch + + + center + True + + + + + + + + + Ignore + + + Taskbar Window + SkipTaskbarSwitch + + + center + True + + + + + + + Last Workspace + SkipLastWorkspaceSwitch + + + center + True + + + + + + + + diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/gschemas.compiled b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..ed44168b95425969c058137faee2a381fb92dc02 GIT binary patch literal 708 zcmZ8ev1%1T6rIGpXrhJ0JV=wyKqAa8en8Ts(Bf4V(RSYM@OF6nW;Q!xHVGtR2xw#B zA4rH`XBr_12q}bsRmyyVjo6BXdd}`*Bp1$}!`aKsnR_3tsnRGLL|b#*k&~1AhTrIfw?4BC3*tdG?5;G{}| z*)YLqkAs9*oUW<)On`_EaI?iN@KlR)yL`$lynjk;h$HZ)|FTdE(G$nuOJM!Q(FrZ; zyXgN0{thhs_z=^lo`Elry8|qofBi+Dn*JPk9yr_Cz0J2$ci?kCPl1D#i&gehci|V{ zOngmiEbQ0vqSTi!)I>m_lOUAog1uZLA4t=2#41bJ%W>PJkl~z}>GB$Np z<41)G84g3mSZG`?G_DsKUoAAgw$K=fn^R11f^K&CoZTRaCr5XJn9GIG`gSK?{N c;fJP>LHS`{mFz>+`D#B`K3pZQ{ia3y1uJ%&3IG5A literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/org.gnome.shell.extensions.auto-activities.gschema.xml b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/org.gnome.shell.extensions.auto-activities.gschema.xml new file mode 100644 index 0000000..a7c7ff6 --- /dev/null +++ b/gnome/.local/share/extensions/auto-activities@CleoMenezesJr.github.io/schemas/org.gnome.shell.extensions.auto-activities.gschema.xml @@ -0,0 +1,29 @@ + + + + + true + + + false + + + true + + + true + + + false + + + true + + + 100 + + + false + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/appfolders.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/appfolders.js new file mode 100644 index 0000000..cabce01 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/appfolders.js @@ -0,0 +1,261 @@ +import Shell from 'gi://Shell'; +import Clutter from 'gi://Clutter'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { PaintSignals } from '../effects/paint_signals.js'; +const Tweener = imports.tweener.tweener; + +const transparent = Clutter.Color.from_pixel(0x00000000); +const FOLDER_DIALOG_ANIMATION_TIME = 200; + +const DIALOGS_STYLES = [ + "appfolder-dialogs-transparent", + "appfolder-dialogs-light", + "appfolder-dialogs-dark" +]; + +let original_zoomAndFadeIn = null; +let original_zoomAndFadeOut = null; +let sigma; +let brightness; + +let _zoomAndFadeIn = function () { + let [sourceX, sourceY] = + this._source.get_transformed_position(); + let [dialogX, dialogY] = + this.child.get_transformed_position(); + + this.child.set({ + translation_x: sourceX - dialogX, + translation_y: sourceY - dialogY, + scale_x: this._source.width / this.child.width, + scale_y: this._source.height / this.child.height, + opacity: 0, + }); + + this.set_background_color(transparent); + + let blur_effect = this.get_effect("appfolder-blur"); + + blur_effect.sigma = 0; + blur_effect.brightness = 1.0; + Tweener.addTween(blur_effect, + { + sigma: sigma, + brightness: brightness, + time: FOLDER_DIALOG_ANIMATION_TIME / 1000, + transition: 'easeOutQuad' + } + ); + + this.child.ease({ + translation_x: 0, + translation_y: 0, + scale_x: 1, + scale_y: 1, + opacity: 255, + duration: FOLDER_DIALOG_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this._needsZoomAndFade = false; + + if (this._sourceMappedId === 0) { + this._sourceMappedId = this._source.connect( + 'notify::mapped', this._zoomAndFadeOut.bind(this)); + } +}; + +let _zoomAndFadeOut = function () { + if (!this._isOpen) + return; + + if (!this._source.mapped) { + this.hide(); + return; + } + + let [sourceX, sourceY] = + this._source.get_transformed_position(); + let [dialogX, dialogY] = + this.child.get_transformed_position(); + + this.set_background_color(transparent); + + let blur_effect = this.get_effect("appfolder-blur"); + Tweener.addTween(blur_effect, + { + sigma: 0, + brightness: 1.0, + time: FOLDER_DIALOG_ANIMATION_TIME / 1000, + transition: 'easeInQuad' + } + ); + + this.child.ease({ + translation_x: sourceX - dialogX, + translation_y: sourceY - dialogY, + scale_x: this._source.width / this.child.width, + scale_y: this._source.height / this.child.height, + opacity: 0, + duration: FOLDER_DIALOG_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this.child.set({ + translation_x: 0, + translation_y: 0, + scale_x: 1, + scale_y: 1, + opacity: 255, + }); + this.hide(); + + this._popdownCallbacks.forEach(func => func()); + this._popdownCallbacks = []; + }, + }); + + this._needsZoomAndFade = false; +}; + + +export const AppFoldersBlur = class AppFoldersBlur { + constructor(connections, settings, _) { + this.connections = connections; + this.paint_signals = new PaintSignals(connections); + this.settings = settings; + } + + enable() { + this._log("blurring appfolders"); + + brightness = this.settings.appfolder.CUSTOMIZE + ? this.settings.appfolder.BRIGHTNESS + : this.settings.BRIGHTNESS; + sigma = this.settings.appfolder.CUSTOMIZE + ? this.settings.appfolder.SIGMA + : this.settings.SIGMA; + + let appDisplay = Main.overview._overview.controls._appDisplay; + + if (appDisplay._folderIcons.length > 0) { + this.blur_appfolders(); + } + + this.connections.connect( + appDisplay, 'view-loaded', this.blur_appfolders.bind(this) + ); + } + + blur_appfolders() { + let appDisplay = Main.overview._overview.controls._appDisplay; + + if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) + this._log(`appfolders hack level ${this.settings.HACKS_LEVEL}`); + + appDisplay._folderIcons.forEach(icon => { + icon._ensureFolderDialog(); + + if (original_zoomAndFadeIn == null) { + original_zoomAndFadeIn = icon._dialog._zoomAndFadeIn; + } + if (original_zoomAndFadeOut == null) { + original_zoomAndFadeOut = icon._dialog._zoomAndFadeOut; + } + + let blur_effect = new Shell.BlurEffect({ + name: "appfolder-blur", + sigma: sigma, + brightness: brightness, + mode: Shell.BlurMode.BACKGROUND + }); + + icon._dialog.remove_effect_by_name("appfolder-blur"); + icon._dialog.add_effect(blur_effect); + + DIALOGS_STYLES.forEach( + style => icon._dialog._viewBox.remove_style_class_name(style) + ); + + if (this.settings.appfolder.STYLE_DIALOGS > 0) + icon._dialog._viewBox.add_style_class_name( + DIALOGS_STYLES[this.settings.appfolder.STYLE_DIALOGS - 1] + ); + + // finally override the builtin functions + icon._dialog._zoomAndFadeIn = _zoomAndFadeIn; + icon._dialog._zoomAndFadeOut = _zoomAndFadeOut; + + + // HACK + // + //`Shell.BlurEffect` does not repaint when shadows are under it. [1] + // + // This does not entirely fix this bug (shadows caused by windows + // still cause artifacts), but it prevents the shadows of the panel + // buttons to cause artifacts on the panel itself + // + // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 + + if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) { + this.paint_signals.disconnect_all_for_actor(icon._dialog); + this.paint_signals.connect(icon._dialog, blur_effect); + } else { + this.paint_signals.disconnect_all(); + } + }); + }; + + set_sigma(s) { + sigma = s; + if (this.settings.appfolder.BLUR) + this.blur_appfolders(); + } + + set_brightness(b) { + brightness = b; + if (this.settings.appfolder.BLUR) + this.blur_appfolders(); + } + + // not implemented for dynamic blur + set_color(c) { } + set_noise_amount(n) { } + set_noise_lightness(l) { } + + disable() { + this._log("removing blur from appfolders"); + + let appDisplay = Main.overview._overview.controls._appDisplay; + + if (original_zoomAndFadeIn != null) { + appDisplay._folderIcons.forEach(icon => { + if (icon._dialog) + icon._dialog._zoomAndFadeIn = original_zoomAndFadeIn; + }); + } + + if (original_zoomAndFadeOut != null) { + appDisplay._folderIcons.forEach(icon => { + if (icon._dialog) + icon._dialog._zoomAndFadeOut = original_zoomAndFadeOut; + }); + } + + appDisplay._folderIcons.forEach(icon => { + if (icon._dialog) { + icon._dialog.remove_effect_by_name("appfolder-blur"); + DIALOGS_STYLES.forEach( + s => icon._dialog._viewBox.remove_style_class_name(s) + ); + } + }); + + this.connections.disconnect_all(); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > appfolders] ${str}`); + } +}; \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/applications.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/applications.js new file mode 100644 index 0000000..8334473 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/applications.js @@ -0,0 +1,548 @@ +import Shell from 'gi://Shell'; +import Clutter from 'gi://Clutter'; +import Meta from 'gi://Meta'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { PaintSignals } from '../effects/paint_signals.js'; +import { ApplicationsService } from '../dbus/services.js'; + + +export const ApplicationsBlur = class ApplicationsBlur { + constructor(connections, settings, _) { + this.connections = connections; + this.settings = settings; + this.paint_signals = new PaintSignals(connections); + + // stores every blurred window + this.window_map = new Map(); + // stores every blur actor + this.blur_actor_map = new Map(); + } + + enable() { + this._log("blurring applications..."); + + // export dbus service for preferences + this.service = new ApplicationsService; + this.service.export(); + + // blur already existing windows + this.update_all_windows(); + + // blur every new window + this.connections.connect( + global.display, + 'window-created', + (_meta_display, meta_window) => { + this._log("window created"); + + if (meta_window) { + let window_actor = meta_window.get_compositor_private(); + this.track_new(window_actor, meta_window); + } + } + ); + + this.connect_to_overview(); + } + + /// Connect to the overview being opened/closed to force the blur being + /// shown on every window of the workspaces viewer. + connect_to_overview() { + this.connections.disconnect_all_for(Main.overview); + + if (this.settings.applications.BLUR_ON_OVERVIEW) { + // when the overview is opened, show every window actors (which + // allows the blur to be shown too) + this.connections.connect( + Main.overview, 'showing', + _ => this.window_map.forEach((meta_window, _pid) => { + let window_actor = meta_window.get_compositor_private(); + window_actor.show(); + }) + ); + + // when the overview is closed, hide every actor that is not on the + // current workspace (to mimic the original behaviour) + this.connections.connect( + Main.overview, 'hidden', + _ => { + let active_workspace = + global.workspace_manager.get_active_workspace(); + + this.window_map.forEach((meta_window, _pid) => { + let window_actor = meta_window.get_compositor_private(); + + if ( + meta_window.get_workspace() !== active_workspace + ) + window_actor.hide(); + }); + } + ); + } + } + + /// Iterate through all existing windows and add blur as needed. + update_all_windows() { + // remove all previously blurred windows, in the case where the + // whitelist was changed + this.window_map.forEach(((_meta_window, pid) => { + this.remove_blur(pid); + })); + + for ( + let i = 0; + i < global.workspace_manager.get_n_workspaces(); + ++i + ) { + let workspace = global.workspace_manager.get_workspace_by_index(i); + let windows = workspace.list_windows(); + + windows.forEach(meta_window => { + let window_actor = meta_window.get_compositor_private(); + + // disconnect previous signals + this.connections.disconnect_all_for(window_actor); + + this.track_new(window_actor, meta_window); + }); + } + } + + /// Adds the needed signals to every new tracked window, and adds blur if + /// needed. + track_new(window_actor, meta_window) { + let pid = ("" + Math.random()).slice(2, 16); + + window_actor['blur_provider_pid'] = pid; + meta_window['blur_provider_pid'] = pid; + + // remove the blur when the window is destroyed + this.connections.connect(window_actor, 'destroy', window_actor => { + let pid = window_actor.blur_provider_pid; + if (this.blur_actor_map.has(pid)) { + this.remove_blur(pid); + } + this.window_map.delete(pid); + }); + + // update the blur when mutter-hint or wm-class is changed + for (const prop of ['mutter-hints', 'wm-class']) { + this.connections.connect( + meta_window, + `notify::${prop}`, + _ => { + let pid = meta_window.blur_provider_pid; + this._log(`${prop} changed for pid ${pid}`); + + let window_actor = meta_window.get_compositor_private(); + this.check_blur(pid, window_actor, meta_window); + } + ); + } + + // update the position and size when the window size changes + this.connections.connect(meta_window, 'size-changed', () => { + if (this.blur_actor_map.has(pid)) { + let allocation = this.compute_allocation(meta_window); + let blur_actor = this.blur_actor_map.get(pid); + blur_actor.x = allocation.x; + blur_actor.y = allocation.y; + blur_actor.width = allocation.width; + blur_actor.height = allocation.height; + } + }); + + this.check_blur(pid, window_actor, meta_window); + } + + /// Checks if the given actor needs to be blurred. + /// + /// In order to be blurred, a window either: + /// - is whitelisted in the user preferences if not enable-all + /// - is not blacklisted if enable-all + /// - has a correct mutter hint, set to `blur-provider=sigma_value` + check_blur(pid, window_actor, meta_window) { + let mutter_hint = meta_window.get_mutter_hints(); + let window_wm_class = meta_window.get_wm_class(); + + let enable_all = this.settings.applications.ENABLE_ALL; + let whitelist = this.settings.applications.WHITELIST; + let blacklist = this.settings.applications.BLACKLIST; + + this._log(`checking blur for ${pid}`); + + // either the window is included in whitelist + if (window_wm_class !== "" + && ((enable_all && !blacklist.includes(window_wm_class)) + || (!enable_all && whitelist.includes(window_wm_class)) + ) + && [ + Meta.FrameType.NORMAL, + Meta.FrameType.DIALOG, + Meta.FrameType.MODAL_DIALOG + ].includes(meta_window.get_frame_type()) + ) { + this._log(`application ${pid} listed, blurring it`); + + // get blur effect parameters + + let brightness, sigma; + + if (this.settings.applications.CUSTOMIZE) { + brightness = this.settings.applications.BRIGHTNESS; + sigma = this.settings.applications.SIGMA; + } else { + brightness = this.settings.BRIGHTNESS; + sigma = this.settings.SIGMA; + } + + this.update_blur(pid, window_actor, meta_window, brightness, sigma); + } + + // or blur is asked by window itself + else if ( + mutter_hint != null && + mutter_hint.includes("blur-provider") + ) { + this._log(`application ${pid} has hint ${mutter_hint}, parsing`); + + // get blur effect parameters + let [brightness, sigma] = this.parse_xprop(mutter_hint); + + this.update_blur(pid, window_actor, meta_window, brightness, sigma); + } + + // remove blur if the mutter hint is no longer valid, and the window + // is not explicitly whitelisted or un-blacklisted + else if (this.blur_actor_map.has(pid)) { + this.remove_blur(pid); + } + } + + /// When given the xprop property, returns the brightness and sigma values + /// matching. If one of the two values is invalid, or missing, then it uses + /// default values. + /// + /// An xprop property is valid if it is in one of the following formats: + /// + /// blur-provider=sigma:60,brightness:0.9 + /// blur-provider=s:10,brightness:0.492 + /// blur-provider=b:1.0,s:16 + /// + /// Brightness is a floating-point between 0.0 and 1.0 included. + /// Sigma is an integer between 0 and 999 included. + /// + /// If sigma is set to 0, then the blur is removed. + /// Setting "default" instead of the two values will make the + /// extension use its default value. + /// + /// Note that no space can be inserted. + /// + parse_xprop(property) { + // set brightness and sigma to default values + let brightness, sigma; + if (this.settings.applications.CUSTOMIZE) { + brightness = this.settings.applications.BRIGHTNESS; + sigma = this.settings.applications.SIGMA; + } else { + brightness = this.settings.BRIGHTNESS; + sigma = this.settings.SIGMA; + } + + // get the argument of the property + let arg = property.match("blur-provider=(.*)"); + this._log(`argument = ${arg}`); + + // if argument is valid, parse it + if (arg != null) { + // verify if there is only one value: in this case, this is sigma + let maybe_sigma = parseInt(arg[1]); + + if ( + !isNaN(maybe_sigma) && + maybe_sigma >= 0 && + maybe_sigma <= 999 + ) { + sigma = maybe_sigma; + } else { + // perform pattern matching + let res_b = arg[1].match("(brightness|b):(default|0?1?\.[0-9]*)"); + let res_s = arg[1].match("(sigma|s):(default|\\d{1,3})"); + + // if values are valid and not default, change them to the xprop one + if ( + res_b != null && res_b[2] !== 'default' + ) { + brightness = parseFloat(res_b[2]); + } + + if ( + res_s != null && res_s[2] !== 'default' + ) { + sigma = parseInt(res_s[2]); + } + } + } + + this._log(`brightness = ${brightness}, sigma = ${sigma}`); + + return [brightness, sigma]; + } + + /// Updates the blur on a window which needs to be blurred. + update_blur(pid, window_actor, meta_window, brightness, sigma) { + // the window is already blurred, update its blur effect + if (this.blur_actor_map.has(pid)) { + // window is already blurred, but sigma is null: remove the blur + if (sigma === 0) { + this.remove_blur(pid); + } + // window is already blurred and sigma is non-null: update it + else { + this.update_blur_effect( + this.blur_actor_map.get(pid), + brightness, + sigma + ); + } + } + + // the window is not blurred, and sigma is a non-null value: blur it + else if (sigma !== 0) { + // window is not blurred, blur it + this.create_blur_effect( + pid, + window_actor, + meta_window, + brightness, + sigma + ); + } + } + + /// Add the blur effect to the window. + create_blur_effect(pid, window_actor, meta_window, brightness, sigma) { + let blur_effect = new Shell.BlurEffect({ + sigma: sigma, + brightness: brightness, + mode: Shell.BlurMode.BACKGROUND + }); + + let blur_actor = this.create_blur_actor( + meta_window, + window_actor, + blur_effect + ); + + // if hacks are selected, force to repaint the window + if (this.settings.HACKS_LEVEL === 1 || this.settings.HACKS_LEVEL === 2) { + this._log("applications hack level 1 or 2"); + + this.paint_signals.disconnect_all(); + this.paint_signals.connect(blur_actor, blur_effect); + } else { + this.paint_signals.disconnect_all(); + } + + // insert the blurred widget + window_actor.insert_child_at_index(blur_actor, 0); + + // make sure window is blurred in overview + if (this.settings.applications.BLUR_ON_OVERVIEW) + this.enforce_window_visibility_on_overview_for(window_actor); + + // set the window actor's opacity + this.set_window_opacity(window_actor, this.settings.applications.OPACITY); + + this.connections.connect( + window_actor, + 'notify::opacity', + _ => this.set_window_opacity(window_actor, this.settings.applications.OPACITY) + ); + + // register the blur actor/effect + blur_actor['blur_provider_pid'] = pid; + this.blur_actor_map.set(pid, blur_actor); + this.window_map.set(pid, meta_window); + + // hide the blur if window is invisible + if (!window_actor.visible) { + blur_actor.hide(); + } + + // hide the blur if window becomes invisible + this.connections.connect( + window_actor, + 'notify::visible', + window_actor => { + let pid = window_actor.blur_provider_pid; + if (window_actor.visible) { + this.blur_actor_map.get(pid).show(); + } else { + this.blur_actor_map.get(pid).hide(); + } + } + ); + } + + /// Makes sure that, when the overview is visible, the window actor will + /// stay visible no matter what. + /// We can instead hide the last child of the window actor, which will + /// improve performances without hiding the blur effect. + enforce_window_visibility_on_overview_for(window_actor) { + this.connections.connect(window_actor, 'notify::visible', + _ => { + if (this.settings.applications.BLUR_ON_OVERVIEW) { + if ( + !window_actor.visible + && Main.overview.visible + ) { + window_actor.show(); + window_actor.get_last_child().hide(); + } + else if ( + window_actor.visible + ) + window_actor.get_last_child().show(); + } + } + ); + } + + /// Set the opacity of the window actor that sits on top of the blur effect. + set_window_opacity(window_actor, opacity) { + window_actor.get_children().forEach(child => { + if (child.name !== "blur-actor" && child.opacity != opacity) + child.opacity = opacity; + }); + } + + /// Compute the size and position for a blur actor. + /// On wayland, it seems like we need to divide by the scale to get the + /// correct result. + compute_allocation(meta_window) { + const is_wayland = Meta.is_wayland_compositor(); + const monitor_index = meta_window.get_monitor(); + // check if the window is using wayland, or xwayland/xorg for rendering + const scale = is_wayland && meta_window.get_client_type() == 0 + ? Main.layoutManager.monitors[monitor_index].geometry_scale + : 1; + + let frame = meta_window.get_frame_rect(); + let buffer = meta_window.get_buffer_rect(); + + return { + x: (frame.x - buffer.x) / scale, + y: (frame.y - buffer.y) / scale, + width: frame.width / scale, + height: frame.height / scale + }; + } + + /// Returns a new already blurred widget, configured to follow the size and + /// position of its target window. + create_blur_actor(meta_window, window_actor, blur_effect) { + // compute the size and position + let allocation = this.compute_allocation(meta_window); + + // create the actor + let blur_actor = new Clutter.Actor({ + x: allocation.x, + y: allocation.y, + width: allocation.width, + height: allocation.height + }); + + // add the effect + blur_actor.add_effect_with_name('blur-effect', blur_effect); + + return blur_actor; + } + + /// Updates the blur effect by overwriting its sigma and brightness values. + update_blur_effect(blur_actor, brightness, sigma) { + let effect = blur_actor.get_effect('blur-effect'); + effect.sigma = sigma; + effect.brightness = brightness; + } + + /// Removes the blur actor from the shell and unregister it. + remove_blur(pid) { + this._log(`removing blur for pid ${pid}`); + + let meta_window = this.window_map.get(pid); + // disconnect needed signals and untrack window + if (meta_window) { + this.window_map.delete(pid); + let window_actor = meta_window.get_compositor_private(); + + let blur_actor = this.blur_actor_map.get(pid); + if (blur_actor) { + this.blur_actor_map.delete(pid); + + if (window_actor) { + // reset the opacity + this.set_window_opacity(window_actor, 255); + + // remove the blurred actor + window_actor.remove_child(blur_actor); + + // disconnect the signals about overview animation etc + this.connections.disconnect_all_for(window_actor); + } + } + } + } + + disable() { + this._log("removing blur from applications..."); + + this.service?.unexport(); + + this.blur_actor_map.forEach(((_blur_actor, pid) => { + this.remove_blur(pid); + })); + + this.connections.disconnect_all(); + this.paint_signals.disconnect_all(); + } + + /// Update the opacity of all window actors. + set_opacity() { + let opacity = this.settings.applications.OPACITY; + + this.window_map.forEach(((meta_window, _pid) => { + let window_actor = meta_window.get_compositor_private(); + this.set_window_opacity(window_actor, opacity); + })); + } + + /// Updates each blur effect to use new sigma value + // FIXME set_sigma and set_brightness are called when the extension is + // loaded and when sigma is changed, and do not respect the per-app + // xprop behaviour + set_sigma(s) { + this.blur_actor_map.forEach((actor, _) => { + actor.get_effect('blur-effect').set_sigma(s); + }); + } + + /// Updates each blur effect to use new brightness value + set_brightness(b) { + this.blur_actor_map.forEach((actor, _) => { + actor.get_effect('blur-effect').set_brightness(b); + }); + } + + // not implemented for dynamic blur + set_color(c) { } + set_noise_amount(n) { } + set_noise_lightness(l) { } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > applications] ${str}`); + } +}; \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/dash_to_dock.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/dash_to_dock.js new file mode 100644 index 0000000..3c56588 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/dash_to_dock.js @@ -0,0 +1,322 @@ +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +const Signals = imports.signals; + +import { PaintSignals } from '../effects/paint_signals.js'; + +const DASH_STYLES = [ + "transparent-dash", + "light-dash", + "dark-dash" +]; + + +/// This type of object is created for every dash found, and talks to the main +/// DashBlur thanks to signals. +/// +/// This allows to dynamically track the created dashes for each screen. +class DashInfos { + constructor(dash_blur, dash, background_parent, effect, settings) { + // the parent DashBlur object, to communicate + this.dash_blur = dash_blur; + // the blurred dash + this.dash = dash; + this.background_parent = background_parent; + this.effect = effect; + this.settings = settings; + this.old_style = this.dash._background.style; + + dash_blur.connections.connect(dash_blur, 'remove-dashes', () => { + this._log("removing blur from dash"); + this.dash.get_parent().remove_child(this.background_parent); + this.dash._background.style = this.old_style; + + DASH_STYLES.forEach( + style => this.dash.remove_style_class_name(style) + ); + }); + + dash_blur.connections.connect(dash_blur, 'update-sigma', () => { + this.effect.sigma = this.dash_blur.sigma; + }); + + dash_blur.connections.connect(dash_blur, 'update-brightness', () => { + this.effect.brightness = this.dash_blur.brightness; + }); + + dash_blur.connections.connect(dash_blur, 'override-background', () => { + this.dash._background.style = null; + + DASH_STYLES.forEach( + style => this.dash.remove_style_class_name(style) + ); + + this.dash.set_style_class_name( + DASH_STYLES[this.settings.dash_to_dock.STYLE_DASH_TO_DOCK] + ); + }); + + dash_blur.connections.connect(dash_blur, 'reset-background', () => { + this.dash._background.style = this.old_style; + + DASH_STYLES.forEach( + style => this.dash.remove_style_class_name(style) + ); + }); + + dash_blur.connections.connect(dash_blur, 'show', () => { + this.effect.sigma = this.dash_blur.sigma; + }); + + dash_blur.connections.connect(dash_blur, 'hide', () => { + this.effect.sigma = 0; + }); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > dash] ${str}`); + } +} + +export const DashBlur = class DashBlur { + constructor(connections, settings, _) { + this.dashes = []; + this.connections = connections; + this.settings = settings; + this.paint_signals = new PaintSignals(connections); + this.sigma = this.settings.dash_to_dock.CUSTOMIZE + ? this.settings.dash_to_dock.SIGMA + : this.settings.SIGMA; + this.brightness = this.settings.dash_to_dock.CUSTOMIZE + ? this.settings.dash_to_dock.BRIGHTNESS + : this.settings.BRIGHTNESS; + this.enabled = false; + } + + enable() { + this.connections.connect(Main.uiGroup, 'actor-added', (_, actor) => { + if ( + (actor.get_name() === "dashtodockContainer") && + (actor.constructor.name === 'DashToDock') + ) + this.try_blur(actor); + }); + + this.blur_existing_dashes(); + this.connect_to_overview(); + + this.enabled = true; + } + + // Finds all existing dashes on every monitor, and call `try_blur` on them + // We cannot only blur `Main.overview.dash`, as there could be several + blur_existing_dashes() { + this._log("searching for dash"); + + // blur every dash found, filtered by name + Main.uiGroup.get_children().filter((child) => { + return (child.get_name() === "dashtodockContainer") && + (child.constructor.name === 'DashToDock'); + }).forEach(this.try_blur.bind(this)); + } + + // Tries to blur the dash contained in the given actor + try_blur(dash_container) { + let dash_box = dash_container._slider.get_child(); + + // verify that we did not already blur that dash + if (!dash_box.get_children().some((child) => { + return child.get_name() === "dash-blurred-background-parent"; + })) { + this._log("dash to dock found, blurring it"); + + // finally blur the dash + let dash = dash_box.get_children().find(child => { + return child.get_name() === 'dash'; + }); + + this.dashes.push(this.blur_dash_from(dash, dash_container)); + } + } + + // Blurs the dash and returns a `DashInfos` containing its information + blur_dash_from(dash, dash_container) { + // the effect to be applied + let effect = new Shell.BlurEffect({ + brightness: this.brightness, + sigma: this.sigma, + mode: Shell.BlurMode.BACKGROUND + }); + + // dash background parent, not visible + let background_parent = new St.Widget({ + name: 'dash-blurred-background-parent', + style_class: 'dash-blurred-background-parent', + width: 0, + height: 0 + }); + + // dash background widget + let background = new St.Widget({ + name: 'dash-blurred-background', + style_class: 'dash-blurred-background', + x: 0, + y: dash_container._slider.y, + width: dash.width, + height: dash.height, + }); + + // updates size and position on change + this.connections.connect(dash_container._slider, 'notify::y', _ => { + background.y = dash_container._slider.y; + }); + this.connections.connect(dash, 'notify::width', _ => { + background.width = dash.width; + }); + this.connections.connect(dash, 'notify::height', _ => { + background.height = dash.height; + }); + + // add the widget to the dash + background.add_effect(effect); + background_parent.add_child(background); + dash.get_parent().insert_child_at_index(background_parent, 0); + + // HACK + // + //`Shell.BlurEffect` does not repaint when shadows are under it. [1] + // + // This does not entirely fix this bug (shadows caused by windows + // still cause artifacts), but it prevents the shadows of the panel + // buttons to cause artifacts on the panel itself + // + // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 + + if (this.settings.HACKS_LEVEL === 1) { + this._log("dash hack level 1"); + this.paint_signals.disconnect_all(); + + let rp = () => { + effect.queue_repaint(); + }; + + dash._box.get_children().forEach((icon) => { + try { + let zone = icon.get_child_at_index(0); + + this.connections.connect(zone, [ + 'enter-event', 'leave-event', 'button-press-event' + ], rp); + } catch (e) { + this._warn(`${e}, continuing`); + } + }); + + this.connections.connect(dash._box, 'actor-added', (_, actor) => { + try { + let zone = actor.get_child_at_index(0); + + this.connections.connect(zone, [ + 'enter-event', 'leave-event', 'button-press-event' + ], rp); + } catch (e) { + this._warn(`${e}, continuing`); + } + }); + + let show_apps = dash._showAppsIcon; + + this.connections.connect(show_apps, [ + 'enter-event', 'leave-event', 'button-press-event' + ], rp); + + this.connections.connect(dash, 'leave-event', rp); + } else if (this.settings.HACKS_LEVEL === 2) { + this._log("dash hack level 2"); + + this.paint_signals.connect(background, effect); + } else { + this.paint_signals.disconnect_all(); + } + + // create infos + let infos = new DashInfos( + this, dash, background_parent, effect, this.settings + ); + + // update the background + this.update_background(); + + // returns infos + return infos; + } + + /// Connect when overview if opened/closed to hide/show the blur accordingly + connect_to_overview() { + this.connections.disconnect_all_for(Main.overview); + + if (this.settings.dash_to_dock.UNBLUR_IN_OVERVIEW) { + this.connections.connect( + Main.overview, 'showing', this.hide.bind(this) + ); + this.connections.connect( + Main.overview, 'hidden', this.show.bind(this) + ); + } + }; + + /// Updates the background to either remove it or not, according to the + /// user preferences. + update_background() { + if (this.settings.dash_to_dock.OVERRIDE_BACKGROUND) + this.emit('override-background', true); + else + this.emit('reset-background', true); + } + + set_sigma(sigma) { + this.sigma = sigma; + this.emit('update-sigma', true); + } + + set_brightness(brightness) { + this.brightness = brightness; + this.emit('update-brightness', true); + } + + // not implemented for dynamic blur + set_color(c) { } + set_noise_amount(n) { } + set_noise_lightness(l) { } + + disable() { + this._log("removing blur from dashes"); + + this.emit('remove-dashes', true); + + this.dashes = []; + this.connections.disconnect_all(); + + this.enabled = false; + } + + show() { + this.emit('show', true); + } + hide() { + this.emit('hide', true); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > dash manager] ${str}`); + } + + _warn(str) { + console.warn(`[Blur my Shell > dash manager] ${str}`); + } +}; + +Signals.addSignalMethods(DashBlur.prototype); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/lockscreen.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/lockscreen.js new file mode 100644 index 0000000..bd10908 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/lockscreen.js @@ -0,0 +1,167 @@ +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as Background from 'resource:///org/gnome/shell/ui/background.js'; +import { UnlockDialog } from 'resource:///org/gnome/shell/ui/unlockDialog.js'; + +let sigma; +let brightness; +let color; +let noise; +let lightness; + +const original_createBackground = + UnlockDialog.prototype._createBackground; +const original_updateBackgroundEffects = + UnlockDialog.prototype._updateBackgroundEffects; + + +export const LockscreenBlur = class LockscreenBlur { + constructor(connections, settings, effects_manager) { + this.connections = connections; + this.settings = settings; + this.effects_manager = effects_manager; + } + + enable() { + this._log("blurring lockscreen"); + + brightness = this.settings.lockscreen.CUSTOMIZE + ? this.settings.lockscreen.BRIGHTNESS + : this.settings.BRIGHTNESS; + sigma = this.settings.lockscreen.CUSTOMIZE + ? this.settings.lockscreen.SIGMA + : this.settings.SIGMA; + color = this.settings.lockscreen.CUSTOMIZE + ? this.settings.lockscreen.COLOR + : this.settings.COLOR; + noise = this.settings.lockscreen.CUSTOMIZE + ? this.settings.lockscreen.NOISE_AMOUNT + : this.settings.NOISE_AMOUNT; + lightness = this.settings.lockscreen.CUSTOMIZE + ? this.settings.lockscreen.NOISE_LIGHTNESS + : this.settings.NOISE_LIGHTNESS; + + this.update_lockscreen(); + } + + update_lockscreen() { + UnlockDialog.prototype._createBackground = + this._createBackground; + UnlockDialog.prototype._updateBackgroundEffects = + this._updateBackgroundEffects; + } + + _createBackground(monitorIndex) { + let monitor = Main.layoutManager.monitors[monitorIndex]; + let widget = new St.Widget({ + style_class: "screen-shield-background", + x: monitor.x, + y: monitor.y, + width: monitor.width, + height: monitor.height, + }); + + let blur_effect = new Shell.BlurEffect({ + name: 'blur', + sigma: sigma, + brightness: brightness + }); + + // store the scale in the effect in order to retrieve later + blur_effect.scale = monitor.geometry_scale; + + let color_effect = global.blur_my_shell._lockscreen_blur.effects_manager.new_color_effect({ + name: 'color', + color: color + }, this.settings); + + let noise_effect = global.blur_my_shell._lockscreen_blur.effects_manager.new_noise_effect({ + name: 'noise', + noise: noise, + lightness: lightness + }, this.settings); + + widget.add_effect(color_effect); + widget.add_effect(noise_effect); + widget.add_effect(blur_effect); + + let bgManager = new Background.BackgroundManager({ + container: widget, + monitorIndex, + controlPosition: false, + }); + + this._bgManagers.push(bgManager); + + this._backgroundGroup.add_child(widget); + } + + _updateBackgroundEffects() { + for (const widget of this._backgroundGroup) { + const color_effect = widget.get_effect('color'); + const noise_effect = widget.get_effect('noise'); + const blur_effect = widget.get_effect('blur'); + + if (color_effect) + color_effect.set({ + color: color + }); + + if (noise_effect) { + noise_effect.set({ + noise: noise, + lightness: lightness, + }); + } + + if (blur_effect) { + blur_effect.set({ + brightness: brightness, + sigma: sigma * blur_effect.scale, + }); + } + } + } + + set_sigma(s) { + sigma = s; + this.update_lockscreen(); + } + + set_brightness(b) { + brightness = b; + this.update_lockscreen(); + } + + set_color(c) { + color = c; + this.update_lockscreen(); + } + + set_noise_amount(n) { + noise = n; + this.update_lockscreen(); + } + + set_noise_lightness(l) { + lightness = l; + this.update_lockscreen(); + } + + disable() { + this._log("removing blur from lockscreen"); + + UnlockDialog.prototype._createBackground = + original_createBackground; + UnlockDialog.prototype._updateBackgroundEffects = + original_updateBackgroundEffects; + + this.connections.disconnect_all(); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > lockscreen] ${str}`); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/overview.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/overview.js new file mode 100644 index 0000000..3e04ac4 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/overview.js @@ -0,0 +1,309 @@ +import Shell from 'gi://Shell'; +import Meta from 'gi://Meta'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { WorkspaceAnimationController } from 'resource:///org/gnome/shell/ui/workspaceAnimation.js'; +const wac_proto = WorkspaceAnimationController.prototype; + +const OVERVIEW_COMPONENTS_STYLE = [ + "overview-components-light", + "overview-components-dark", + "overview-components-transparent" +]; + + +export const OverviewBlur = class OverviewBlur { + constructor(connections, settings, effects_manager) { + this.connections = connections; + this.effects = []; + this.settings = settings; + this.effects_manager = effects_manager; + this._workspace_switch_bg_actors = []; + this.enabled = false; + } + + enable() { + this._log("blurring overview"); + + // connect to every background change (even without changing image) + // FIXME this signal is fired very often, so we should find another one + // fired only when necessary (but that still catches all cases) + this.connections.connect( + Main.layoutManager._backgroundGroup, + 'notify', + _ => { + this._log("updated background"); + this.update_backgrounds(); + } + ); + + // connect to monitors change + this.connections.connect( + Main.layoutManager, + 'monitors-changed', + _ => { + if (Main.screenShield && !Main.screenShield.locked) { + this._log("changed monitors"); + this.update_backgrounds(); + } + } + ); + + // add css class name for workspace-switch background + Main.uiGroup.add_style_class_name("blurred-overview"); + + // add css class name to make components semi-transparent if wanted + this.update_components_classname(); + + // update backgrounds when the component is enabled + this.update_backgrounds(); + + + // part for the workspace animation switch + + // make sure not to do this part if the extension was enabled prior, as + // the functions would call themselves and cause infinite recursion + if (!this.enabled) { + // store original workspace switching methods for restoring them on + // disable() + this._original_PrepareSwitch = wac_proto._prepareWorkspaceSwitch; + this._original_FinishSwitch = wac_proto._finishWorkspaceSwitch; + + const w_m = global.workspace_manager; + const outer_this = this; + + // create a blurred background actor for each monitor during a + // workspace switch + wac_proto._prepareWorkspaceSwitch = function (...params) { + outer_this._log("prepare workspace switch"); + outer_this._original_PrepareSwitch.apply(this, params); + + // this permits to show the blur behind windows that are on + // workspaces on the left and right + if ( + outer_this.settings.applications.BLUR + ) { + let ws_index = w_m.get_active_workspace_index(); + [ws_index - 1, ws_index + 1].forEach( + i => w_m.get_workspace_by_index(i)?.list_windows().forEach( + window => window.get_compositor_private().show() + ) + ); + } + + Main.layoutManager.monitors.forEach(monitor => { + if ( + !( + Meta.prefs_get_workspaces_only_on_primary() && + (monitor !== Main.layoutManager.primaryMonitor) + ) + ) { + const bg_actor = outer_this.create_background_actor( + monitor, true + ); + + Main.uiGroup.insert_child_above( + bg_actor, + global.window_group + ); + + // store the actors so that we can delete them later + outer_this._workspace_switch_bg_actors.push(bg_actor); + } + }); + }; + + // remove the workspace-switch actors when the switch is done + wac_proto._finishWorkspaceSwitch = function (...params) { + outer_this._log("finish workspace switch"); + outer_this._original_FinishSwitch.apply(this, params); + + // this hides windows that are not on the current workspace + if ( + outer_this.settings.applications.BLUR + ) + for (let i = 0; i < w_m.get_n_workspaces(); i++) { + if (i != w_m.get_active_workspace_index()) + w_m.get_workspace_by_index(i)?.list_windows().forEach( + window => window.get_compositor_private().hide() + ); + } + + outer_this.effects = outer_this.effects.filter( + effects_group => !effects_group.is_transition + ); + + outer_this._workspace_switch_bg_actors.forEach(actor => { + actor.destroy(); + }); + outer_this._workspace_switch_bg_actors = []; + }; + } + + this.enabled = true; + } + + update_backgrounds() { + // remove every old background + this.remove_background_actors(); + + // add new backgrounds + Main.layoutManager.monitors.forEach(monitor => { + const bg_actor = this.create_background_actor(monitor, false); + + Main.layoutManager.overviewGroup.insert_child_at_index( + bg_actor, + monitor.index + ); + }); + } + + create_background_actor(monitor, is_transition) { + let bg_actor = new Meta.BackgroundActor({ + meta_display: global.display, + monitor: monitor.index + }); + let background_group = Main.layoutManager._backgroundGroup + .get_children() + .filter((child) => child instanceof Meta.BackgroundActor); + let background = + background_group[ + Main.layoutManager.monitors.length - monitor.index - 1 + ]; + + if (!background) { + this._warn("could not get background for overview"); + return bg_actor; + } + + bg_actor.content.set({ + background: background.get_content().background + }); + + let blur_effect = new Shell.BlurEffect({ + brightness: this.settings.overview.CUSTOMIZE + ? this.settings.overview.BRIGHTNESS + : this.settings.BRIGHTNESS, + sigma: this.settings.overview.CUSTOMIZE + ? this.settings.overview.SIGMA + : this.settings.SIGMA + * monitor.geometry_scale, + mode: Shell.BlurMode.ACTOR + }); + + // store the scale in the effect in order to retrieve it in set_sigma + blur_effect.scale = monitor.geometry_scale; + + let color_effect = this.effects_manager.new_color_effect({ + color: this.settings.overview.CUSTOMIZE + ? this.settings.overview.COLOR + : this.settings.COLOR + }, this.settings); + + let noise_effect = this.effects_manager.new_noise_effect({ + noise: this.settings.overview.CUSTOMIZE + ? this.settings.overview.NOISE_AMOUNT + : this.settings.NOISE_AMOUNT, + lightness: this.settings.overview.CUSTOMIZE + ? this.settings.overview.NOISE_LIGHTNESS + : this.settings.NOISE_LIGHTNESS + }, this.settings); + + bg_actor.add_effect(color_effect); + bg_actor.add_effect(noise_effect); + bg_actor.add_effect(blur_effect); + this.effects.push({ blur_effect, color_effect, noise_effect, is_transition }); + + bg_actor.set_x(monitor.x); + bg_actor.set_y(monitor.y); + + return bg_actor; + } + + /// Updates the classname to style overview components with semi-transparent + /// backgrounds. + update_components_classname() { + OVERVIEW_COMPONENTS_STYLE.forEach( + style => Main.uiGroup.remove_style_class_name(style) + ); + + if (this.settings.overview.STYLE_COMPONENTS > 0) + Main.uiGroup.add_style_class_name( + OVERVIEW_COMPONENTS_STYLE[this.settings.overview.STYLE_COMPONENTS - 1] + ); + } + + set_sigma(s) { + this.effects.forEach(effect => { + effect.blur_effect.sigma = s * effect.blur_effect.scale; + }); + } + + set_brightness(b) { + this.effects.forEach(effect => { + effect.blur_effect.brightness = b; + }); + } + + set_color(c) { + this.effects.forEach(effect => { + effect.color_effect.color = c; + }); + } + + set_noise_amount(n) { + this.effects.forEach(effect => { + effect.noise_effect.noise = n; + }); + } + + set_noise_lightness(l) { + this.effects.forEach(effect => { + effect.noise_effect.lightness = l; + }); + } + + remove_background_actors() { + Main.layoutManager.overviewGroup.get_children().forEach(actor => { + if (actor.constructor.name === 'Meta_BackgroundActor') { + actor.get_effects().forEach(effect => { + this.effects_manager.remove(effect); + }); + Main.layoutManager.overviewGroup.remove_child(actor); + actor.destroy(); + } + }); + this.effects = []; + } + + disable() { + this._log("removing blur from overview"); + this.remove_background_actors(); + Main.uiGroup.remove_style_class_name("blurred-overview"); + OVERVIEW_COMPONENTS_STYLE.forEach( + style => Main.uiGroup.remove_style_class_name(style) + ); + + // make sure to absolutely not do this if the component was not enabled + // prior, as this would cause infinite recursion + if (this.enabled) { + // restore original behavior + if (this._original_PrepareSwitch) + wac_proto._prepareWorkspaceSwitch = this._original_PrepareSwitch; + if (this._original_FinishSwitch) + wac_proto._finishWorkspaceSwitch = this._original_FinishSwitch; + } + + this.connections.disconnect_all(); + this.enabled = false; + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > overview] ${str}`); + } + + _warn(str) { + console.warn(`[Blur my Shell > overview] ${str}`); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/panel.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/panel.js new file mode 100644 index 0000000..a904f92 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/panel.js @@ -0,0 +1,695 @@ +import St from 'gi://St'; +import Shell from 'gi://Shell'; +import Meta from 'gi://Meta'; +import Mtk from 'gi://Mtk'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { PaintSignals } from '../effects/paint_signals.js'; + +const DASH_TO_PANEL_UUID = 'dash-to-panel@jderose9.github.com'; +const PANEL_STYLES = [ + "transparent-panel", + "light-panel", + "dark-panel", + "contrasted-panel" +]; + + +export const PanelBlur = class PanelBlur { + constructor(connections, settings, effects_manager) { + this.connections = connections; + this.window_signal_ids = new Map(); + this.settings = settings; + this.effects_manager = effects_manager; + this.actors_list = []; + this.enabled = false; + } + + enable() { + this._log("blurring top panel"); + + // check for panels when Dash to Panel is activated + this.connections.connect( + Main.extensionManager, + 'extension-state-changed', + (_, extension) => { + if (extension.uuid === DASH_TO_PANEL_UUID + && extension.state === 1 + ) { + this.connections.connect( + global.dashToPanel, + 'panels-created', + _ => this.blur_dtp_panels() + ); + + this.blur_existing_panels(); + } + } + ); + + this.blur_existing_panels(); + + // connect to overview being opened/closed, and dynamically show or not + // the blur when a window is near a panel + this.connect_to_windows_and_overview(); + + // connect to every background change (even without changing image) + // FIXME this signal is fired very often, so we should find another one + // fired only when necessary (but that still catches all cases) + this.connections.connect( + Main.layoutManager._backgroundGroup, + 'notify', + _ => this.actors_list.forEach(actors => + this.update_wallpaper(actors) + ) + ); + + // connect to monitors change + this.connections.connect( + Main.layoutManager, + 'monitors-changed', + _ => { + if (Main.screenShield && !Main.screenShield.locked) { + this.reset(); + } + } + ); + + this.enabled = true; + } + + reset() { + this._log("resetting..."); + + this.disable(); + setTimeout(_ => this.enable(), 1); + } + + /// Check for already existing panels and blur them if they are not already + blur_existing_panels() { + // check if dash-to-panel is present + if (global.dashToPanel) { + // blur already existing ones + if (global.dashToPanel.panels) + this.blur_dtp_panels(); + } else { + // if no dash-to-panel, blur the main and only panel + this.maybe_blur_panel(Main.panel); + } + } + + blur_dtp_panels() { + // FIXME when Dash to Panel changes its size, it seems it creates new + // panels; but I can't get to delete old widgets + + // blur every panel found + global.dashToPanel.panels.forEach(p => { + this.maybe_blur_panel(p.panel); + }); + + // if main panel is not included in the previous panels, blur it + if ( + !global.dashToPanel.panels + .map(p => p.panel) + .includes(Main.panel) + && + this.settings.dash_to_panel.BLUR_ORIGINAL_PANEL + ) + this.maybe_blur_panel(Main.panel); + }; + + /// Blur a panel only if it is not already blurred (contained in the list) + maybe_blur_panel(panel) { + // check if the panel is contained in the list + let actors = this.actors_list.find( + actors => actors.widgets.panel == panel + ); + + if (!actors) + // if the actors is not blurred, blur it + this.blur_panel(panel); + else + // if it is blurred, update the blur anyway + this.change_blur_type(actors); + } + + /// Blur a panel + blur_panel(panel) { + let panel_box = panel.get_parent(); + let is_dtp_panel = false; + if (!panel_box.name) { + is_dtp_panel = true; + panel_box = panel_box.get_parent(); + } + + let monitor = this.find_monitor_for(panel); + if (!monitor) + return; + + let background_parent = new St.Widget({ + name: 'topbar-blurred-background-parent', + x: 0, y: 0, width: 0, height: 0 + }); + + let background = this.settings.panel.STATIC_BLUR + ? new Meta.BackgroundActor({ + meta_display: global.display, + monitor: monitor.index + }) + : new St.Widget; + + background_parent.add_child(background); + + // insert background parent + panel_box.insert_child_at_index(background_parent, 0); + + let blur = new Shell.BlurEffect({ + brightness: this.settings.panel.CUSTOMIZE + ? this.settings.panel.BRIGHTNESS + : this.settings.BRIGHTNESS, + sigma: this.settings.panel.CUSTOMIZE + ? this.settings.panel.SIGMA + : this.settings.SIGMA + * monitor.geometry_scale, + mode: this.settings.panel.STATIC_BLUR + ? Shell.BlurMode.ACTOR + : Shell.BlurMode.BACKGROUND + }); + + // store the scale in the effect in order to retrieve it in set_sigma + blur.scale = monitor.geometry_scale; + + let color = this.effects_manager.new_color_effect({ + color: this.settings.panel.CUSTOMIZE + ? this.settings.panel.COLOR + : this.settings.COLOR + }, this.settings); + + let noise = this.effects_manager.new_noise_effect({ + noise: this.settings.panel.CUSTOMIZE + ? this.settings.panel.NOISE_AMOUNT + : this.settings.NOISE_AMOUNT, + lightness: this.settings.panel.CUSTOMIZE + ? this.settings.panel.NOISE_LIGHTNESS + : this.settings.NOISE_LIGHTNESS + }, this.settings); + + let paint_signals = new PaintSignals(this.connections); + + let actors = { + widgets: { panel, panel_box, background, background_parent }, + effects: { blur, color, noise }, + paint_signals, + monitor, + is_dtp_panel + }; + + this.actors_list.push(actors); + + // perform updates + this.change_blur_type(actors); + + // connect to panel, panel_box and its parent position or size change + // this should fire update_size every time one of its params change + this.connections.connect( + panel, + 'notify::position', + _ => this.update_size(actors) + ); + this.connections.connect( + panel_box, + ['notify::size', 'notify::position'], + _ => this.update_size(actors) + ); + this.connections.connect( + panel_box.get_parent(), + 'notify::position', + _ => this.update_size(actors) + ); + } + + update_all_blur_type() { + this.actors_list.forEach(actors => this.change_blur_type(actors)); + } + + change_blur_type(actors) { + let is_static = this.settings.panel.STATIC_BLUR; + + // reset widgets to right state + actors.widgets.background_parent.remove_child(actors.widgets.background); + this.effects_manager.remove(actors.effects.blur); + this.effects_manager.remove(actors.effects.color); + this.effects_manager.remove(actors.effects.noise); + + // create new background actor + actors.widgets.background = is_static + ? new Meta.BackgroundActor({ + meta_display: global.display, + monitor: this.find_monitor_for(actors.widgets.panel).index + }) + : new St.Widget; + + // change blur mode + actors.effects.blur.set_mode(is_static ? 0 : 1); + + // disable other effects if the blur is dynamic, as they makes it opaque + actors.effects.color._static = is_static; + actors.effects.noise._static = is_static; + actors.effects.color.update_enabled(); + actors.effects.noise.update_enabled(); + + // add the effects in order + actors.widgets.background.add_effect(actors.effects.color); + actors.widgets.background.add_effect(actors.effects.noise); + actors.widgets.background.add_effect(actors.effects.blur); + + // add the background actor behing the panel + actors.widgets.background_parent.add_child(actors.widgets.background); + + // perform updates + this.update_wallpaper(actors); + this.update_size(actors); + + + // HACK + // + //`Shell.BlurEffect` does not repaint when shadows are under it. [1] + // + // This does not entirely fix this bug (shadows caused by windows + // still cause artifacts), but it prevents the shadows of the panel + // buttons to cause artifacts on the panel itself + // + // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 + + if (!is_static) { + if (this.settings.HACKS_LEVEL === 1) { + this._log("panel hack level 1"); + actors.paint_signals.disconnect_all(); + + let rp = () => { actors.effects.blur.queue_repaint(); }; + + this.connections.connect(actors.widgets.panel, [ + 'enter-event', 'leave-event', 'button-press-event' + ], rp); + + actors.widgets.panel.get_children().forEach(child => { + this.connections.connect(child, [ + 'enter-event', 'leave-event', 'button-press-event' + ], rp); + }); + } else if (this.settings.HACKS_LEVEL === 2) { + this._log("panel hack level 2"); + actors.paint_signals.disconnect_all(); + + actors.paint_signals.connect( + actors.widgets.background, actors.effects.blur + ); + } else { + actors.paint_signals.disconnect_all(); + } + } + } + + update_wallpaper(actors) { + // if static blur, get right wallpaper and update blur with it + if (this.settings.panel.STATIC_BLUR) { + let bg = Main.layoutManager._backgroundGroup.get_child_at_index( + Main.layoutManager.monitors.length + - this.find_monitor_for(actors.widgets.panel).index - 1 + ); + if (bg) + actors.widgets.background.content.set({ + background: bg.get_content().background + }); + else + this._warn("could not get background for panel"); + } + } + + update_size(actors) { + let panel = actors.widgets.panel; + let panel_box = actors.widgets.panel_box; + let background = actors.widgets.background; + let monitor = this.find_monitor_for(panel); + if (!monitor) + return; + + let [width, height] = panel_box.get_size(); + background.width = width; + background.height = height; + + // if static blur, need to clip the background + if (this.settings.panel.STATIC_BLUR) { + // an alternative to panel.get_transformed_position, because it + // sometimes yields NaN (probably when the actor is not fully + // positionned yet) + let [p_x, p_y] = panel_box.get_position(); + let [p_p_x, p_p_y] = panel_box.get_parent().get_position(); + let x = p_x + p_p_x - monitor.x; + let y = p_y + p_p_y - monitor.y; + + background.set_clip(x, y, width, height); + background.x = -x; + background.y = -y; + + // fixes a bug where the blur is washed away when changing the sigma + this.invalidate_blur(actors); + } else { + background.x = panel.x; + background.y = panel.y; + } + + // update the monitor panel is on + actors.monitor = this.find_monitor_for(panel); + } + + /// An helper function to find the monitor in which an actor is situated, + /// there might be a pre-existing function in GLib already + find_monitor_for(actor) { + let extents = actor.get_transformed_extents(); + let rect = new Mtk.Rectangle({ + x: extents.get_x(), + y: extents.get_y(), + width: extents.get_width(), + height: extents.get_height(), + }); + + let index = global.display.get_monitor_index_for_rect(rect); + + return Main.layoutManager.monitors[index]; + } + + /// Connect when overview if opened/closed to hide/show the blur accordingly + /// + /// If HIDETOPBAR is set, we need just to hide the blur when showing appgrid + /// (so no shadow is cropped) + connect_to_overview() { + // may be called when panel blur is disabled, if hidetopbar + // compatibility is toggled on/off + // if this is the case, do nothing as only the panel blur interfers with + // hidetopbar + if ( + this.settings.panel.BLUR && + this.settings.panel.UNBLUR_IN_OVERVIEW + ) { + if (!this.settings.hidetopbar.COMPATIBILITY) { + this.connections.connect( + Main.overview, 'showing', this.hide.bind(this) + ); + this.connections.connect( + Main.overview, 'hidden', this.show.bind(this) + ); + } else { + let appDisplay = Main.overview._overview._controls._appDisplay; + + this.connections.connect( + appDisplay, 'show', this.hide.bind(this) + ); + this.connections.connect( + appDisplay, 'hide', this.show.bind(this) + ); + } + + } + } + + /// Connect to windows disable transparency when a window is too close + connect_to_windows() { + if ( + this.settings.panel.OVERRIDE_BACKGROUND_DYNAMICALLY + ) { + // connect to overview opening/closing + this.connections.connect(Main.overview, ['showing', 'hiding'], + this.update_visibility.bind(this) + ); + + // connect to session mode update + this.connections.connect(Main.sessionMode, 'updated', + this.update_visibility.bind(this) + ); + + // manage already-existing windows + for (const meta_window_actor of global.get_window_actors()) { + this.on_window_actor_added( + meta_window_actor.get_parent(), meta_window_actor + ); + } + + // manage windows at their creation/removal + this.connections.connect(global.window_group, 'actor-added', + this.on_window_actor_added.bind(this) + ); + this.connections.connect(global.window_group, 'actor-removed', + this.on_window_actor_removed.bind(this) + ); + + // connect to a workspace change + this.connections.connect(global.window_manager, 'switch-workspace', + this.update_visibility.bind(this) + ); + + // perform early update + this.update_visibility(); + } else { + // reset transparency for every panels + this.actors_list.forEach( + actors => this.set_should_override_panel(actors, true) + ); + } + } + + /// An helper to connect to both the windows and overview signals. + /// This is the only function that should be directly called, to prevent + /// inconsistencies with signals not being disconnected. + connect_to_windows_and_overview() { + this.disconnect_from_windows_and_overview(); + this.connect_to_overview(); + this.connect_to_windows(); + } + + /// Disconnect all the connections created by connect_to_windows + disconnect_from_windows_and_overview() { + // disconnect the connections to actors + for (const actor of [ + Main.overview, Main.sessionMode, + global.window_group, global.window_manager, + Main.overview._overview._controls._appDisplay + ]) { + this.connections.disconnect_all_for(actor); + } + + // disconnect the connections from windows + for (const [actor, ids] of this.window_signal_ids) { + for (const id of ids) { + actor.disconnect(id); + } + } + this.window_signal_ids = new Map(); + } + + /// Callback when a new window is added + on_window_actor_added(container, meta_window_actor) { + this.window_signal_ids.set(meta_window_actor, [ + meta_window_actor.connect('notify::allocation', + _ => this.update_visibility() + ), + meta_window_actor.connect('notify::visible', + _ => this.update_visibility() + ) + ]); + this.update_visibility(); + } + + /// Callback when a window is removed + on_window_actor_removed(container, meta_window_actor) { + for (const signalId of this.window_signal_ids.get(meta_window_actor)) { + meta_window_actor.disconnect(signalId); + } + this.window_signal_ids.delete(meta_window_actor); + this.update_visibility(); + } + + /// Update the visibility of the blur effect + update_visibility() { + if ( + Main.panel.has_style_pseudo_class('overview') + || !Main.sessionMode.hasWindows + ) { + this.actors_list.forEach( + actors => this.set_should_override_panel(actors, true) + ); + return; + } + + if (!Main.layoutManager.primaryMonitor) + return; + + // get all the windows in the active workspace that are visible + const workspace = global.workspace_manager.get_active_workspace(); + const windows = workspace.list_windows().filter(meta_window => + meta_window.showing_on_its_workspace() + && !meta_window.is_hidden() + && meta_window.get_window_type() !== Meta.WindowType.DESKTOP + // exclude Desktop Icons NG + && meta_window.get_gtk_application_id() !== "com.rastersoft.ding" + ); + + // check if at least one window is near enough to each panel and act + // accordingly + const scale = St.ThemeContext.get_for_stage(global.stage).scale_factor; + this.actors_list + // do not apply for dtp panels, as it would only cause bugs and it + // can be done from its preferences anyway + .filter(actors => !actors.is_dtp_panel) + .forEach(actors => { + let panel = actors.widgets.panel; + let panel_top = panel.get_transformed_position()[1]; + let panel_bottom = panel_top + panel.get_height(); + + // check if at least a window is near enough the panel + let window_overlap_panel = false; + windows.forEach(meta_window => { + let window_monitor_i = meta_window.get_monitor(); + let same_monitor = actors.monitor.index == window_monitor_i; + + let window_vertical_pos = meta_window.get_frame_rect().y; + + // if so, and if in the same monitor, then it overlaps + if (same_monitor + && + window_vertical_pos < panel_bottom + 5 * scale + ) + window_overlap_panel = true; + }); + + // if no window overlaps, then the panel is transparent + this.set_should_override_panel( + actors, !window_overlap_panel + ); + }); + } + + /// Choose wether or not the panel background should be overriden, in + /// respect to its argument and the `override-background` setting. + set_should_override_panel(actors, should_override) { + let panel = actors.widgets.panel; + + PANEL_STYLES.forEach(style => panel.remove_style_class_name(style)); + + if ( + this.settings.panel.OVERRIDE_BACKGROUND + && + should_override + ) + panel.add_style_class_name( + PANEL_STYLES[this.settings.panel.STYLE_PANEL] + ); + } + + /// Fixes a bug where the blur is washed away when changing the sigma, or + /// enabling/disabling other effects. + invalidate_blur(actors) { + if (this.settings.panel.STATIC_BLUR && actors.widgets.background) + actors.widgets.background.get_content().invalidate(); + } + + invalidate_all_blur() { + this.actors_list.forEach(actors => this.invalidate_blur(actors)); + } + + set_sigma(s) { + this.actors_list.forEach(actors => { + actors.effects.blur.sigma = s * actors.effects.blur.scale; + this.invalidate_blur(actors); + }); + } + + set_brightness(b) { + this.actors_list.forEach(actors => { + actors.effects.blur.brightness = b; + }); + } + + set_color(c) { + this.actors_list.forEach(actors => { + actors.effects.color.color = c; + }); + } + + set_noise_amount(n) { + this.actors_list.forEach(actors => { + actors.effects.noise.noise = n; + }); + } + + set_noise_lightness(l) { + this.actors_list.forEach(actors => { + actors.effects.noise.lightness = l; + }); + } + + show() { + this.actors_list.forEach(actors => { + actors.widgets.background_parent.show(); + }); + } + + hide() { + this.actors_list.forEach(actors => { + actors.widgets.background_parent.hide(); + }); + } + + // destroy every blurred background left, necessary after sleep + destroy_blur_effects() { + Main.panel?.get_parent()?.get_children().forEach( + child => { + if (child.name === 'topbar-blurred-background-parent') { + child.get_children().forEach(meta_background_actor => { + meta_background_actor.get_effects().forEach(effect => { + this.effects_manager.remove(effect); + }); + }); + child.destroy_all_children(); + child.destroy(); + } + } + ); + } + + disable() { + this._log("removing blur from top panel"); + + this.disconnect_from_windows_and_overview(); + + this.actors_list.forEach(actors => { + this.set_should_override_panel(actors, false); + this.effects_manager.remove(actors.effects.noise); + this.effects_manager.remove(actors.effects.color); + this.effects_manager.remove(actors.effects.blur); + try { + actors.widgets.panel_box.remove_child( + actors.widgets.background_parent + ); + } catch (e) { } + actors.widgets.background_parent?.destroy(); + }); + + this.destroy_blur_effects(); + + this.actors_list = []; + + this.connections.disconnect_all(); + + this.enabled = false; + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > panel] ${str}`); + } + + _warn(str) { + console.warn(`[Blur my Shell > panel] ${str}`); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/screenshot.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/screenshot.js new file mode 100644 index 0000000..34bb3c4 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/screenshot.js @@ -0,0 +1,173 @@ +import Shell from 'gi://Shell'; +import Meta from 'gi://Meta'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + + +export const ScreenshotBlur = class ScreenshotBlur { + constructor(connections, settings, effects_manager) { + this.connections = connections; + this.effects = []; + this.settings = settings; + this.effects_manager = effects_manager; + } + + enable() { + this._log("blurring screenshot's window selector"); + + // connect to every background change (even without changing image) + // FIXME this signal is fired very often, so we should find another one + // fired only when necessary (but that still catches all cases) + this.connections.connect( + Main.layoutManager._backgroundGroup, + 'notify', + _ => { + this._log("updated background for screenshot's window selector"); + this.update_backgrounds(); + } + ); + + // connect to monitors change + this.connections.connect( + Main.layoutManager, + 'monitors-changed', + _ => { + if (Main.screenShield && !Main.screenShield.locked) { + this._log("changed monitors for screenshot's window selector"); + this.update_backgrounds(); + } + } + ); + + // update backgrounds when the component is enabled + this.update_backgrounds(); + } + + update_backgrounds() { + // remove every old background + this.remove(); + + // add new backgrounds + for (let i = 0; i < Main.screenshotUI._windowSelectors.length; i++) { + const actor = Main.screenshotUI._windowSelectors[i]; + const monitor = Main.layoutManager.monitors[i]; + + if (!monitor) + continue; + + const bg_actor = this.create_background_actor(monitor); + actor.insert_child_at_index(bg_actor, 0); + actor._blur_actor = bg_actor; + } + } + + create_background_actor(monitor) { + let bg_actor = new Meta.BackgroundActor({ + meta_display: global.display, + monitor: monitor.index + }); + let background = Main.layoutManager._backgroundGroup.get_child_at_index( + Main.layoutManager.monitors.length - monitor.index - 1 + ); + + if (!background) { + this._warn("could not get background for screenshot's window selector"); + return bg_actor; + } + + bg_actor.content.set({ + background: background.get_content().background + }); + + let blur_effect = new Shell.BlurEffect({ + brightness: this.settings.screenshot.CUSTOMIZE + ? this.settings.screenshot.BRIGHTNESS + : this.settings.BRIGHTNESS, + sigma: this.settings.screenshot.CUSTOMIZE + ? this.settings.screenshot.SIGMA + : this.settings.SIGMA + * monitor.geometry_scale, + mode: Shell.BlurMode.ACTOR + }); + + // store the scale in the effect in order to retrieve it in set_sigma + blur_effect.scale = monitor.geometry_scale; + + let color_effect = this.effects_manager.new_color_effect({ + color: this.settings.screenshot.CUSTOMIZE + ? this.settings.screenshot.COLOR + : this.settings.COLOR + }, this.settings); + + let noise_effect = this.effects_manager.new_noise_effect({ + noise: this.settings.screenshot.CUSTOMIZE + ? this.settings.screenshot.NOISE_AMOUNT + : this.settings.NOISE_AMOUNT, + lightness: this.settings.screenshot.CUSTOMIZE + ? this.settings.screenshot.NOISE_LIGHTNESS + : this.settings.NOISE_LIGHTNESS + }, this.settings); + + bg_actor.add_effect(color_effect); + bg_actor.add_effect(noise_effect); + bg_actor.add_effect(blur_effect); + this.effects.push({ blur_effect, color_effect, noise_effect }); + + return bg_actor; + } + + set_sigma(s) { + this.effects.forEach(effect => { + effect.blur_effect.sigma = s * effect.blur_effect; + }); + } + + set_brightness(b) { + this.effects.forEach(effect => { + effect.blur_effect.brightness = b; + }); + } + + set_color(c) { + this.effects.forEach(effect => { + effect.color_effect.color = c; + }); + } + + set_noise_amount(n) { + this.effects.forEach(effect => { + effect.noise_effect.noise = n; + }); + } + + set_noise_lightness(l) { + this.effects.forEach(effect => { + effect.noise_effect.lightness = l; + }); + } + + remove() { + Main.screenshotUI._windowSelectors.forEach(actor => { + if (actor._blur_actor) { + actor.remove_child(actor._blur_actor); + actor._blur_actor.destroy(); + } + }); + this.effects = []; + } + + disable() { + this._log("removing blur from screenshot's window selector"); + + this.remove(); + this.connections.disconnect_all(); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > screenshot] ${str}`); + } + + _warn(str) { + console.warn(`[Blur my Shell > screenshot] ${str}`); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/components/window_list.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/window_list.js new file mode 100644 index 0000000..6a60564 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/components/window_list.js @@ -0,0 +1,162 @@ +import Shell from 'gi://Shell'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { PaintSignals } from '../effects/paint_signals.js'; + + +export const WindowListBlur = class WindowListBlur { + constructor(connections, settings, _) { + this.connections = connections; + this.settings = settings; + this.paint_signals = new PaintSignals(connections); + this.effects = []; + } + + enable() { + this._log("blurring window list"); + + // blur if window-list is found + Main.layoutManager.uiGroup.get_children().forEach( + child => this.try_blur(child) + ); + + // listen to new actors in `Main.layoutManager.uiGroup` and blur it if + // if is window-list + this.connections.connect( + Main.layoutManager.uiGroup, + 'actor-added', + (_, child) => this.try_blur(child) + ); + + // connect to overview + this.connections.connect(Main.overview, 'showing', _ => { + this.hide(); + }); + this.connections.connect(Main.overview, 'hidden', _ => { + this.show(); + }); + } + + try_blur(child) { + if ( + child.constructor.name === "WindowList" && + child.style !== "background:transparent;" + ) { + this._log("found window list to blur"); + + let blur_effect = new Shell.BlurEffect({ + name: 'window-list-blur', + sigma: this.settings.window_list.CUSTOMIZE + ? this.settings.window_list.SIGMA + : this.settings.SIGMA, + brightness: this.settings.window_list.CUSTOMIZE + ? this.settings.window_list.BRIGHTNESS + : this.settings.BRIGHTNESS, + mode: Shell.BlurMode.BACKGROUND + }); + + child.set_style("background:transparent;"); + child.add_effect(blur_effect); + this.effects.push({ blur_effect }); + + child._windowList.get_children().forEach( + window => this.blur_window_button(window) + ); + + this.connections.connect( + child._windowList, + 'actor-added', + (_, window) => this.blur_window_button(window) + ); + + + // HACK + // + //`Shell.BlurEffect` does not repaint when shadows are under it. [1] + // + // This does not entirely fix this bug (shadows caused by windows + // still cause artifacts), but it prevents the shadows of the panel + // buttons to cause artifacts on the panel itself + // + // [1]: https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/2857 + + if (this.settings.HACKS_LEVEL === 1) { + this._log("window list hack level 1"); + + this.paint_signals.connect(child, blur_effect); + + } else if (this.settings.HACKS_LEVEL === 2) { + this._log("window list hack level 2"); + + this.paint_signals.connect(child, blur_effect); + } else { + this.paint_signals.disconnect_all(); + } + } + } + + blur_window_button(window) { + window.get_child_at_index(0).set_style( + "box-shadow:none; background-color:rgba(0,0,0,0.2); border-radius:5px;" + ); + } + + try_remove_blur(child) { + if ( + child.constructor.name === "WindowList" && + child.style === "background:transparent;" + ) { + child.style = null; + child.remove_effect_by_name('window-list-blur'); + + child._windowList.get_children().forEach( + child => child.get_child_at_index(0).set_style(null) + ); + } + } + + set_sigma(s) { + this.effects.forEach(effect => { + effect.blur_effect.sigma = s; + }); + } + + set_brightness(b) { + this.effects.forEach(effect => { + effect.blur_effect.brightness = b; + }); + } + + // not implemented for dynamic blur + set_color(c) { } + set_noise_amount(n) { } + set_noise_lightness(l) { } + + hide() { + this.set_sigma(0); + } + + show() { + this.set_sigma( + this.settings.window_list.CUSTOMIZE + ? this.settings.window_list.SIGMA + : this.settings.SIGMA + ); + } + + disable() { + this._log("removing blur from window list"); + + Main.layoutManager.uiGroup.get_children().forEach( + child => this.try_remove_blur(child) + ); + + this.effects = []; + this.connections.disconnect_all(); + } + + _log(str) { + if (this.settings.DEBUG) + console.log(`[Blur my Shell > window list] ${str}`); + } +}; \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/connections.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/connections.js new file mode 100644 index 0000000..c15ef48 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/connections.js @@ -0,0 +1,101 @@ +import GObject from 'gi://GObject'; + +/// An object to easily manage signals. +export const Connections = class Connections { + constructor() { + this.buffer = []; + } + + /// Adds a connection. + /// + /// Takes as arguments: + /// - an actor, which fires the signal + /// - signal(s) (string or array of strings), which are watched for + /// - a callback, which is called when the signal is fired + connect(actor, signals, handler) { + if (signals instanceof Array) { + signals.forEach(signal => { + let id = actor.connect(signal, handler); + this.process_connection(actor, id); + }); + } else { + let id = actor.connect(signals, handler); + this.process_connection(actor, id); + } + + } + + /// Process the given actor and id. + /// + /// This makes sure that the signal is disconnected when the actor is + /// destroyed, and that the signal can be managed through other Connections + /// methods. + process_connection(actor, id) { + let infos = { + actor: actor, + id: id + }; + + // remove the signal when the actor is destroyed + if ( + actor.connect && + ( + !(actor instanceof GObject.Object) || + GObject.signal_lookup('destroy', actor) + ) + ) { + let destroy_id = actor.connect('destroy', () => { + actor.disconnect(id); + actor.disconnect(destroy_id); + + let index = this.buffer.indexOf(infos); + if (index >= 0) { + this.buffer.splice(index, 1); + } + }); + } + + this.buffer.push(infos); + } + + /// Disconnects every connection found for an actor. + disconnect_all_for(actor) { + // get every connection stored for the actor + let actor_connections = this.buffer.filter( + infos => infos.actor === actor + ); + + // remove each of them + actor_connections.forEach((connection) => { + // disconnect + try { + connection.actor.disconnect(connection.id); + } catch (e) { + this._warn(`error removing connection: ${e}; continuing`); + } + + // remove from buffer + let index = this.buffer.indexOf(connection); + this.buffer.splice(index, 1); + }); + } + + /// Disconnect every connection for each actor. + disconnect_all() { + this.buffer.forEach((connection) => { + // disconnect + try { + connection.actor.disconnect(connection.id); + } catch (e) { + this._warn(`error removing connection: ${e}; continuing`); + } + }); + + // reset buffer + this.buffer = []; + } + + _warn(str) { + console.warn(`[Blur my Shell > connections] ${str}`); + } +}; \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/effects_manager.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/effects_manager.js new file mode 100644 index 0000000..7ef7b22 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/effects_manager.js @@ -0,0 +1,89 @@ +import { ColorEffect } from '../effects/color_effect.js'; +import { NoiseEffect } from '../effects/noise_effect.js'; + + +/// An object to manage effects (by not destroying them all the time) +export const EffectsManager = class EffectsManager { + constructor(connections) { + this.connections = connections; + this.used = []; + this.color_effects = []; + this.noise_effects = []; + } + + connect_to_destroy(effect) { + effect.old_actor = effect.get_actor(); + if (effect.old_actor) + effect.old_actor_id = effect.old_actor.connect('destroy', _ => { + this.remove(effect); + }); + + this.connections.connect(effect, 'notify::actor', _ => { + let actor = effect.get_actor(); + + if (effect.old_actor && actor != effect.old_actor) + effect.old_actor.disconnect(effect.old_actor_id); + + if (actor) { + effect.old_actor_id = actor.connect('destroy', _ => { + this.remove(effect); + }); + } + }); + } + + new_color_effect(params, settings) { + let effect; + if (this.color_effects.length > 0) { + effect = this.color_effects.splice(0, 1)[0]; + effect.set(params); + } else + effect = new ColorEffect(params, settings); + + this.used.push(effect); + this.connect_to_destroy(effect); + return effect; + } + + new_noise_effect(params, settings) { + let effect; + if (this.noise_effects.length > 0) { + effect = this.noise_effects.splice(0, 1)[0]; + effect.set(params); + } else + effect = new NoiseEffect(params, settings); + + this.used.push(effect); + this.connect_to_destroy(effect); + return effect; + } + + remove(effect) { + effect.get_actor()?.remove_effect(effect); + if (effect.old_actor) + effect.old_actor.disconnect(effect.old_actor_id); + delete effect.old_actor; + delete effect.old_actor_id; + + let index = this.used.indexOf(effect); + if (index >= 0) { + this.used.splice(index, 1); + + if (effect instanceof ColorEffect) + this.color_effects.push(effect); + else if (effect instanceof NoiseEffect) + this.noise_effects.push(effect); + } + } + + destroy_all() { + this.used.forEach(effect => { this.remove(effect); }); + [ + this.used, + this.color_effects, + this.noise_effects + ].forEach(array => { + array.splice(0, array.length); + }); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/keys.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/keys.js new file mode 100644 index 0000000..5a87ba4 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/keys.js @@ -0,0 +1,131 @@ +import { Type } from './settings.js'; + +// This lists the preferences keys +export const Keys = [ + { + component: "general", schemas: [ + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.B, name: "color-and-noise" }, + { type: Type.I, name: "hacks-level" }, + { type: Type.B, name: "debug" }, + ] + }, + { + component: "overview", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.I, name: "style-components" }, + ] + }, + { + component: "appfolder", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.I, name: "style-dialogs" }, + ] + }, + { + component: "panel", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.B, name: "static-blur" }, + { type: Type.B, name: "unblur-in-overview" }, + { type: Type.B, name: "override-background" }, + { type: Type.I, name: "style-panel" }, + { type: Type.B, name: "override-background-dynamically" }, + ] + }, + { + component: "dash-to-dock", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.B, name: "static-blur" }, + { type: Type.B, name: "unblur-in-overview" }, + { type: Type.B, name: "override-background" }, + { type: Type.I, name: "style-dash-to-dock" }, + ] + }, + { + component: "applications", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + { type: Type.I, name: "opacity" }, + { type: Type.B, name: "blur-on-overview" }, + { type: Type.B, name: "enable-all" }, + { type: Type.AS, name: "whitelist" }, + { type: Type.AS, name: "blacklist" }, + ] + }, + { + component: "lockscreen", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + ] + }, + { + component: "window-list", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + ] + }, + { + component: "screenshot", schemas: [ + { type: Type.B, name: "blur" }, + { type: Type.B, name: "customize" }, + { type: Type.I, name: "sigma" }, + { type: Type.D, name: "brightness" }, + { type: Type.C, name: "color" }, + { type: Type.D, name: "noise-amount" }, + { type: Type.D, name: "noise-lightness" }, + ] + }, + { + component: "hidetopbar", schemas: [ + { type: Type.B, name: "compatibility" }, + ] + }, + { + component: "dash-to-panel", schemas: [ + { type: Type.B, name: "blur-original-panel" }, + ] + }, +]; \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/settings.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/settings.js new file mode 100644 index 0000000..d2bef17 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/conveniences/settings.js @@ -0,0 +1,182 @@ +import GLib from 'gi://GLib'; + +const Signals = imports.signals; + +/// An enum non-extensively describing the type of gsettings key. +export const Type = { + B: 'Boolean', + I: 'Integer', + D: 'Double', + S: 'String', + C: 'Color', + AS: 'StringArray' +}; + +/// An object to get and manage the gsettings preferences. +/// +/// Should be initialized with an array of keys, for example: +/// +/// let settings = new Settings([ +/// { type: Type.I, name: "panel-corner-radius" }, +/// { type: Type.B, name: "debug" } +/// ]); +/// +/// Each {type, name} object represents a gsettings key, which must be created +/// in the gschemas.xml file of the extension. +export const Settings = class Settings { + constructor(keys, settings) { + this.settings = settings; + this.keys = keys; + + this.keys.forEach(bundle => { + let component = this; + let component_settings = settings; + if (bundle.component !== "general") { + let bundle_component = bundle.component.replaceAll('-', '_'); + this[bundle_component] = { + settings: this.settings.get_child(bundle.component) + }; + component = this[bundle_component]; + component_settings = settings.get_child(bundle.component); + } + + + bundle.schemas.forEach(key => { + let property_name = this.get_property_name(key.name); + + switch (key.type) { + case Type.B: + Object.defineProperty(component, property_name, { + get() { + return component_settings.get_boolean(key.name); + }, + set(v) { + component_settings.set_boolean(key.name, v); + } + }); + break; + + case Type.I: + Object.defineProperty(component, property_name, { + get() { + return component_settings.get_int(key.name); + }, + set(v) { + component_settings.set_int(key.name, v); + } + }); + break; + + case Type.D: + Object.defineProperty(component, property_name, { + get() { + return component_settings.get_double(key.name); + }, + set(v) { + component_settings.set_double(key.name, v); + } + }); + break; + + case Type.S: + Object.defineProperty(component, property_name, { + get() { + return component_settings.get_string(key.name); + }, + set(v) { + component_settings.set_string(key.name, v); + } + }); + break; + + case Type.C: + Object.defineProperty(component, property_name, { + // returns the array [red, blue, green, alpha] with + // values between 0 and 1 + get() { + let val = component_settings.get_value(key.name); + return val.deep_unpack(); + }, + // takes an array [red, blue, green, alpha] with + // values between 0 and 1 + set(v) { + let val = new GLib.Variant("(dddd)", v); + component_settings.set_value(key.name, val); + } + }); + break; + + case Type.AS: + Object.defineProperty(component, property_name, { + get() { + let val = component_settings.get_value(key.name); + return val.deep_unpack(); + }, + set(v) { + let val = new GLib.Variant("as", v); + component_settings.set_value(key.name, val); + } + }); + break; + } + + component[property_name + '_reset'] = function () { + return component_settings.reset(key.name); + }; + + component[property_name + '_changed'] = function (cb) { + return component_settings.connect('changed::' + key.name, cb); + }; + + component[property_name + '_disconnect'] = function () { + return component_settings.disconnect.apply( + component_settings, arguments + ); + }; + }); + }); + }; + + /// Reset the preferences. + reset() { + this.keys.forEach(bundle => { + let component = this; + if (bundle.component !== "general") { + let bundle_component = bundle.component.replaceAll('-', '_'); + component = this[bundle_component]; + } + + bundle.schemas.forEach(key => { + let property_name = this.get_property_name(key.name); + component[property_name + '_reset'](); + }); + }); + + this.emit('reset', true); + } + + /// From the gschema name, returns the name of the associated property on + /// the Settings object. + get_property_name(name) { + return name.replaceAll('-', '_').toUpperCase(); + } + + /// Remove all connections managed by the Settings object, i.e. created with + /// `settings.PROPERTY_changed(callback)`. + disconnect_all_settings() { + this.keys.forEach(bundle => { + let component = this; + if (bundle.component !== "general") { + let bundle_component = bundle.component.replaceAll('-', '_'); + component = this[bundle_component]; + } + + bundle.schemas.forEach(key => { + let property_name = this.get_property_name(key.name); + component[property_name + '_disconnect'](); + }); + }); + } +}; + +Signals.addSignalMethods(Settings.prototype); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/client.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/client.js new file mode 100644 index 0000000..3867b3a --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/client.js @@ -0,0 +1,58 @@ +import Gio from 'gi://Gio'; + +const bus_name = 'org.gnome.Shell'; +const iface_name = 'dev.aunetx.BlurMyShell'; +const obj_path = '/dev/aunetx/BlurMyShell'; + + +/// Call pick() from the DBus service, it will open the Inspector from +/// gnome-shell to pick an actor on stage. +export function pick() { + Gio.DBus.session.call( + bus_name, + obj_path, + iface_name, + 'pick', + null, + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + null + ); +} + +/// Connect to DBus 'picking' signal, which will be emitted when the inspector +/// is picking a window. +export function on_picking(cb) { + const id = Gio.DBus.session.signal_subscribe( + bus_name, + iface_name, + 'picking', + obj_path, + null, + Gio.DBusSignalFlags.NONE, + _ => { + cb(); + Gio.DBus.session.signal_unsubscribe(id); + } + ); +} + +/// Connect to DBus 'picked' signal, which will be emitted when a window is +/// picked. +export function on_picked(cb) { + const id = Gio.DBus.session.signal_subscribe( + bus_name, + iface_name, + 'picked', + obj_path, + null, + Gio.DBusSignalFlags.NONE, + (conn, sender, obj_path, iface, signal, params) => { + const val = params.get_child_value(0); + cb(val.get_string()[0]); + Gio.DBus.session.signal_unsubscribe(id); + } + ); +} diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/iface.xml b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/iface.xml new file mode 100644 index 0000000..4f298ad --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/iface.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/services.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/services.js new file mode 100644 index 0000000..9b566c2 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/dbus/services.js @@ -0,0 +1,90 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as LookingGlass from 'resource:///org/gnome/shell/ui/lookingGlass.js'; + + +export const ApplicationsService = class ApplicationsService { + constructor() { + let decoder = new TextDecoder(); + let path = GLib.filename_from_uri(GLib.uri_resolve_relative( + import.meta.url, 'iface.xml', GLib.UriFlags.NONE) + )[0]; + let [, buffer] = GLib.file_get_contents(path); + let iface = decoder.decode(buffer); + GLib.free(buffer); + + this.DBusImpl = Gio.DBusExportedObject.wrapJSObject(iface, this); + } + + /// Pick Window for Preferences Page, exported to DBus client. + pick() { + // emit `picking` signal to know we are listening + const send_picking_signal = _ => + this.DBusImpl.emit_signal( + 'picking', + null + ); + + // emit `picked` signal to send wm_class + const send_picked_signal = wm_class => + this.DBusImpl.emit_signal( + 'picked', + new GLib.Variant('(s)', [wm_class]) + ); + + // notify the preferences that we are listening + send_picking_signal(); + + // A very interesting way to pick a window: + // 1. Open LookingGlass to mask all event handles of window + // 2. Use inspector to pick window, thats is also lookingGlass do + // 3. Close LookingGlass when done + // It will restore event handles of window + + // open then hide LookingGlass + const looking_class = Main.createLookingGlass(); + looking_class.open(); + looking_class.hide(); + + // inspect window now + const inspector = new LookingGlass.Inspector(Main.createLookingGlass()); + inspector.connect('target', (me, target, x, y) => { + // remove border effect when window is picked. + const effect_name = 'lookingGlass_RedBorderEffect'; + target + .get_effects() + .filter(e => e.toString().includes(effect_name)) + .forEach(e => target.remove_effect(e)); + + // get wm_class_instance property of window, then pass it to DBus + // client + const type_str = target.toString(); + + let actor = target; + if (type_str.includes('MetaSurfaceActor')) + actor = target.get_parent(); + + if (!actor.toString().includes('WindowActor')) + return send_picked_signal('window-not-found'); + + send_picked_signal( + actor.meta_window.get_wm_class() ?? 'window-not-found' + ); + }); + + // close LookingGlass when we're done + inspector.connect('closed', _ => looking_class.close()); + } + + export() { + this.DBusImpl.export( + Gio.DBus.session, + '/dev/aunetx/BlurMyShell' + ); + }; + + unexport() { + this.DBusImpl.unexport(); + } +}; diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.glsl b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.glsl new file mode 100644 index 0000000..142b89b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.glsl @@ -0,0 +1,13 @@ +uniform sampler2D tex; +uniform float red; +uniform float green; +uniform float blue; +uniform float blend; + +void main() { + vec4 c = texture2D(tex, cogl_tex_coord_in[0].st); + vec3 pix_color = c.xyz; + vec3 color = vec3(red, green, blue); + + cogl_color_out = vec4(mix(pix_color, color, blend), 1.); +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.js new file mode 100644 index 0000000..2ae9191 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/color_effect.js @@ -0,0 +1,181 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Clutter from 'gi://Clutter'; +import Shell from 'gi://Shell'; + + +const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'color_effect.glsl', GLib.UriFlags.NONE))[0]; + + +const get_shader_source = _ => { + try { + return Shell.get_file_contents_utf8_sync(SHADER_PATH); + } catch (e) { + console.warn(`[Blur my Shell] error loading shader from ${SHADER_PATH}: ${e}`); + return null; + } +}; + +/// New Clutter Shader Effect that simply mixes a color in, the class applies +/// the GLSL shader programmed into vfunc_get_static_shader_source and applies +/// it to an Actor. +/// +/// Clutter Shader Source Code: +/// https://github.com/GNOME/clutter/blob/master/clutter/clutter-shader-effect.c +/// +/// GJS Doc: +/// https://gjs-docs.gnome.org/clutter10~10_api/clutter.shadereffect +export const ColorEffect = new GObject.registerClass({ + GTypeName: "ColorEffect", + Properties: { + 'red': GObject.ParamSpec.double( + `red`, + `Red`, + `Red value in shader`, + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 0.4, + ), + 'green': GObject.ParamSpec.double( + `green`, + `Green`, + `Green value in shader`, + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 0.4, + ), + 'blue': GObject.ParamSpec.double( + `blue`, + `Blue`, + `Blue value in shader`, + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 0.4, + ), + 'blend': GObject.ParamSpec.double( + `blend`, + `Blend`, + `Amount of blending between the colors`, + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 0.4, + ), + } +}, class ColorShader extends Clutter.ShaderEffect { + constructor(params, settings) { + // initialize without color as a parameter + let _color = params.color; + delete params.color; + + super(params); + + this._red = null; + this._green = null; + this._blue = null; + this._blend = null; + + this._static = true; + this._settings = settings; + + // set shader source + this._source = get_shader_source(); + if (this._source) + this.set_shader_source(this._source); + + // set shader color + if (_color) + this.color = _color; + + this.update_enabled(); + } + + get red() { + return this._red; + } + + set red(value) { + if (this._red !== value) { + this._red = value; + + this.set_uniform_value('red', parseFloat(this._red - 1e-6)); + } + } + + get green() { + return this._green; + } + + set green(value) { + if (this._green !== value) { + this._green = value; + + this.set_uniform_value('green', parseFloat(this._green - 1e-6)); + } + } + + get blue() { + return this._blue; + } + + set blue(value) { + if (this._blue !== value) { + this._blue = value; + + this.set_uniform_value('blue', parseFloat(this._blue - 1e-6)); + } + } + + get blend() { + return this._blend; + } + + set blend(value) { + if (this._blend !== value) { + this._blend = value; + + this.set_uniform_value('blend', parseFloat(this._blend - 1e-6)); + } + this.update_enabled(); + } + + set color(rgba) { + let [r, g, b, a] = rgba; + this.red = r; + this.green = g; + this.blue = b; + this.blend = a; + } + + get color() { + return [this.red, this.green, this.blue, this.blend]; + } + + /// False set function, only cares about the color. Too hard to change. + set(params) { + this.color = params.color; + } + + update_enabled() { + // don't anything if this._settings is undefined (when calling super) + if (this._settings === undefined) + return; + + this.set_enabled( + this.blend > 0 && + this._settings.COLOR_AND_NOISE && + this._static + ); + } + + + vfunc_paint_target(paint_node = null, paint_context = null) { + this.set_uniform_value("tex", 0); + + if (paint_node && paint_context) + super.vfunc_paint_target(paint_node, paint_context); + else if (paint_node) + super.vfunc_paint_target(paint_node); + else + super.vfunc_paint_target(); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.glsl b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.glsl new file mode 100644 index 0000000..6d701b6 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.glsl @@ -0,0 +1,20 @@ +uniform sampler2D tex; +uniform float noise; +uniform float lightness; + +float PHI = 1.61803398874989484820459; +float SEED = 24; + +float noise_gen(in vec2 xy) { + float r = fract(tan(distance(xy * PHI, xy) * SEED) * xy.x); + r = r != r ? 0.0 : r; + return r; +} + +void main() { + vec4 c = texture2D(tex, cogl_tex_coord_in[0].st); + vec3 pix_color = c.xyz; + float blend = noise * (1. - noise_gen(gl_FragCoord.xy)); + + cogl_color_out = vec4(mix(pix_color, lightness * pix_color, blend), 1.); +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.js new file mode 100644 index 0000000..8d9464c --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/noise_effect.js @@ -0,0 +1,109 @@ +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Clutter from 'gi://Clutter'; +import Shell from 'gi://Shell'; + + +const SHADER_PATH = GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, 'noise_effect.glsl', GLib.UriFlags.NONE))[0]; + +const get_shader_source = _ => { + try { + return Shell.get_file_contents_utf8_sync(SHADER_PATH); + } catch (e) { + console.warn(`[Blur my Shell] error loading shader from ${SHADER_PATH}: ${e}`); + return null; + } +}; + +export const NoiseEffect = new GObject.registerClass({ + GTypeName: "NoiseEffect", + Properties: { + 'noise': GObject.ParamSpec.double( + `noise`, + `Noise`, + `Amount of noise integrated with the image`, + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 0.4, + ), + 'lightness': GObject.ParamSpec.double( + `lightness`, + `Lightness`, + `Lightness of the grey used for the noise`, + GObject.ParamFlags.READWRITE, + 0.0, 2.0, + 0.4, + ), + } +}, class NoiseShader extends Clutter.ShaderEffect { + constructor(params, settings) { + super(params); + + this._noise = null; + this._lightness = null; + + this._static = true; + this._settings = settings; + + if (params.noise) + this.noise = params.noise; + if (params.lightness) + this.lightness = params.lightness; + + // set shader source + this._source = get_shader_source(); + if (this._source) + this.set_shader_source(this._source); + + this.update_enabled(); + } + + get noise() { + return this._noise; + } + + set noise(value) { + if (this._noise !== value) { + this._noise = value; + + this.set_uniform_value('noise', parseFloat(this._noise - 1e-6)); + } + this.update_enabled(); + } + + get lightness() { + return this._lightness; + } + + set lightness(value) { + if (this._lightness !== value) { + this._lightness = value; + + this.set_uniform_value('lightness', parseFloat(this._lightness - 1e-6)); + } + } + + update_enabled() { + // don't anything if this._settings is undefined (when calling super) + if (this._settings === undefined) + return; + + this.set_enabled( + this.noise > 0 && + this._settings.COLOR_AND_NOISE && + this._static + ); + } + + + vfunc_paint_target(paint_node = null, paint_context = null) { + this.set_uniform_value("tex", 0); + + if (paint_node && paint_context) + super.vfunc_paint_target(paint_node, paint_context); + else if (paint_node) + super.vfunc_paint_target(paint_node); + else + super.vfunc_paint_target(); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/paint_signals.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/paint_signals.js new file mode 100644 index 0000000..84a895b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/effects/paint_signals.js @@ -0,0 +1,89 @@ +import GObject from 'gi://GObject'; +import Clutter from 'gi://Clutter'; + + +export const PaintSignals = class PaintSignals { + constructor(connections) { + this.buffer = []; + this.connections = connections; + } + + connect(actor, blur_effect) { + let paint_effect = new EmitPaintSignal(); + let infos = { + actor: actor, + paint_effect: paint_effect + }; + let counter = 0; + + actor.add_effect(paint_effect); + this.connections.connect(paint_effect, 'update-blur', () => { + try { + // checking if blur_effect.queue_repaint() has been recently called + if (counter === 0) { + counter = 2; + blur_effect.queue_repaint(); + } + else counter--; + } catch (e) { } + }); + + // remove the actor from buffer when it is destroyed + if ( + actor.connect && + ( + !(actor instanceof GObject.Object) || + GObject.signal_lookup('destroy', actor) + ) + ) + this.connections.connect(actor, 'destroy', () => { + this.buffer.forEach(infos => { + if (infos.actor === actor) { + // remove from buffer + let index = this.buffer.indexOf(infos); + this.buffer.splice(index, 1); + } + }); + }); + + this.buffer.push(infos); + } + + disconnect_all_for_actor(actor) { + this.buffer.forEach(infos => { + if (infos.actor === actor) { + this.connections.disconnect_all_for(infos.paint_effect); + infos.actor.remove_effect(infos.paint_effect); + + // remove from buffer + let index = this.buffer.indexOf(infos); + this.buffer.splice(index, 1); + } + }); + } + + disconnect_all() { + this.buffer.forEach(infos => { + this.connections.disconnect_all_for(infos.paint_effect); + infos.actor.remove_effect(infos.paint_effect); + }); + + this.buffer = []; + } +}; + +export const EmitPaintSignal = GObject.registerClass({ + GTypeName: 'EmitPaintSignal', + Signals: { + 'update-blur': { + param_types: [] + }, + } +}, + class EmitPaintSignal extends Clutter.Effect { + vfunc_paint(node, paint_context, paint_flags) { + this.emit("update-blur"); + super.vfunc_paint(node, paint_context, paint_flags); + } + } +); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/extension.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/extension.js new file mode 100644 index 0000000..5aa7b9f --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/extension.js @@ -0,0 +1,641 @@ +import Meta from 'gi://Meta'; +import Clutter from 'gi://Clutter'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; + +import { EffectsManager } from './conveniences/effects_manager.js'; +import { Connections } from './conveniences/connections.js'; +import { Settings } from './conveniences/settings.js'; +import { Keys } from './conveniences/keys.js'; + +import { PanelBlur } from './components/panel.js'; +import { OverviewBlur } from './components/overview.js'; +import { DashBlur } from './components/dash_to_dock.js'; +import { LockscreenBlur } from './components/lockscreen.js'; +import { AppFoldersBlur } from './components/appfolders.js'; +import { WindowListBlur } from './components/window_list.js'; +import { ApplicationsBlur } from './components/applications.js'; +import { ScreenshotBlur } from './components/screenshot.js'; + +// This lists the components that need to be connected in order to either use +// general sigma/brightness or their own. +const INDEPENDENT_COMPONENTS = [ + "overview", "appfolder", "panel", "dash_to_dock", "applications", + "lockscreen", "window_list", "screenshot" +]; + + +/// The main extension class, created when the GNOME Shell is loaded. +export default class BlurMyShell extends Extension { + /// Enables the extension + enable() { + // add the extension to global to make it accessible to other extensions + // create it first as it is very useful when debugging crashes + global.blur_my_shell = this; + + // create a Settings instance, to manage extension's preferences + // it needs to be loaded before logging, as it checks for DEBUG + this._settings = new Settings(Keys, this.getSettings()); + + this._log("enabling extension..."); + + // create main extension Connections instance + this._connection = new Connections; + + // store it in a global array + this._connections = [this._connection]; + + // create a global effects manager (to prevent RAM bleeding) + this._effects_manager = new EffectsManager(this._connection); + + // create an instance of each component, with its associated Connections + let init = _ => { + // create a Connections instance, to manage signals + let connection = new Connections; + + // store it to keeps track of them globally + this._connections.push(connection); + + return [connection, this._settings, this._effects_manager]; + }; + + this._panel_blur = new PanelBlur(...init()); + this._dash_to_dock_blur = new DashBlur(...init()); + this._overview_blur = new OverviewBlur(...init()); + this._lockscreen_blur = new LockscreenBlur(...init()); + this._appfolder_blur = new AppFoldersBlur(...init()); + this._window_list_blur = new WindowListBlur(...init()); + this._applications_blur = new ApplicationsBlur(...init()); + this._screenshot_blur = new ScreenshotBlur(...init()); + + // maybe disable clipped redraw + this._update_clipped_redraws(); + + // connect each component to preferences change + this._connect_to_settings(); + + // enable every component + // if the shell is still starting up, wait for it to be entirely loaded; + // this should prevent bugs like #136 and #137 + if (Main.layoutManager._startingUp) { + this._connection.connect( + Main.layoutManager, + 'startup-complete', + this._enable_components.bind(this) + ); + } else { + this._enable_components(); + } + + // try to enable the components as soon as possible anyway, this way the + // overview may load before the user sees it + try { + if (this._settings.overview.BLUR && !this._overview_blur.enabled) + this._overview_blur.enable(); + } catch (e) { + this._log("Could not enable overview blur directly"); + this._log(e); + } + try { + if (this._settings.dash_to_dock.BLUR + && !this._dash_to_dock_blur.enabled) + this._dash_to_dock_blur.enable(); + } catch (e) { + this._log("Could not enable dash-to-dock blur directly"); + this._log(e); + } + try { + if (this._settings.panel.BLUR && !this._panel_blur.enabled) + this._panel_blur.enable(); + } catch (e) { + this._log("Could not enable panel blur directly"); + this._log(e); + } + } + + /// Disables the extension + disable() { + this._log("disabling extension..."); + + // disable every component + this._panel_blur.disable(); + this._dash_to_dock_blur.disable(); + this._overview_blur.disable(); + this._lockscreen_blur.disable(); + this._appfolder_blur.disable(); + this._window_list_blur.disable(); + this._applications_blur.disable(); + this._screenshot_blur.disable(); + + // untrack them + this._panel_blur = null; + this._dash_to_dock_blur = null; + this._overview_blur = null; + this._lockscreen_blur = null; + this._appfolder_blur = null; + this._window_list_blur = null; + this._applications_blur = null; + + // make sure no settings change can re-enable them + this._settings.disconnect_all_settings(); + + // force disconnecting every signal, even if component crashed + this._connections.forEach((connections) => { + connections.disconnect_all(); + }); + this._connections = []; + + // remove the clipped redraws flag + this._reenable_clipped_redraws(); + + // remove the extension from GJS's global + delete global.blur_my_shell; + + this._log("extension disabled."); + + this._settings = null; + } + + /// Restart the extension. + _restart() { + this._log("restarting..."); + + this.disable(); + this.enable(); + + this._log("restarted."); + } + + /// Add or remove the clutter debug flag to disable clipped redraws. + /// This will entirely fix the blur effect, but should not be used except if + /// the user really needs it, as clipped redraws are a huge performance + /// boost for the compositor. + _update_clipped_redraws() { + if (this._settings.HACKS_LEVEL === 3) + this._disable_clipped_redraws(); + else + this._reenable_clipped_redraws(); + } + + /// Add the Clutter debug flag. + _disable_clipped_redraws() { + Meta.add_clutter_debug_flags( + null, Clutter.DrawDebugFlag.DISABLE_CLIPPED_REDRAWS, null + ); + } + + /// Remove the Clutter debug flag. + _reenable_clipped_redraws() { + Meta.remove_clutter_debug_flags( + null, Clutter.DrawDebugFlag.DISABLE_CLIPPED_REDRAWS, null + ); + } + + /// Enables every component needed, should be called when the shell is + /// entirely loaded as the `enable` methods interact with it. + _enable_components() { + // enable each component if needed, and if it is not already enabled + + if (this._settings.panel.BLUR && !this._panel_blur.enabled) + this._panel_blur.enable(); + + if (this._settings.dash_to_dock.BLUR && !this._dash_to_dock_blur.enabled) + this._dash_to_dock_blur.enable(); + + if (this._settings.overview.BLUR && !this._overview_blur.enabled) + this._overview_blur.enable(); + + if (this._settings.lockscreen.BLUR) + this._lockscreen_blur.enable(); + + if (this._settings.appfolder.BLUR) + this._appfolder_blur.enable(); + + if (this._settings.applications.BLUR) + this._applications_blur.enable(); + + if (this._settings.window_list.BLUR) + this._window_list_blur.enable(); + + if (this._settings.screenshot.BLUR) + this._screenshot_blur.enable(); + + this._log("all components enabled."); + } + + /// Updates needed things in each component when a preference changed + _connect_to_settings() { + + // global blur values changed, update everybody + + this._settings.SIGMA_changed(() => { + this._update_sigma(); + }); + this._settings.BRIGHTNESS_changed(() => { + this._update_brightness(); + }); + this._settings.COLOR_changed(() => { + this._update_color(); + }); + this._settings.NOISE_AMOUNT_changed(() => { + this._update_noise_amount(); + }); + this._settings.NOISE_LIGHTNESS_changed(() => { + this._update_noise_lightness(); + }); + this._settings.COLOR_AND_NOISE_changed(() => { + // both updating noise amount and color calls `update_enabled` on + // each color and noise effects + this._update_noise_amount(); + this._update_color(); + }); + + // restart the extension when hacks level is changed, easier than + // restarting individual components and should not happen often either + this._settings.HACKS_LEVEL_changed(_ => this._restart()); + + // connect each component to use the proper sigma/brightness/color + INDEPENDENT_COMPONENTS.forEach(component => { + this._connect_to_individual_settings(component); + }); + + // other component's preferences changed + + // ---------- OVERVIEW ---------- + + // toggled on/off + this._settings.overview.BLUR_changed(() => { + if (this._settings.overview.BLUR) { + this._overview_blur.enable(); + } else { + this._overview_blur.disable(); + } + }); + + // overview components style changed + this._settings.overview.STYLE_COMPONENTS_changed(() => { + if (this._settings.overview.BLUR) { + this._overview_blur.update_components_classname(); + } + }); + + + // ---------- APPFOLDER ---------- + + // toggled on/off + this._settings.appfolder.BLUR_changed(() => { + if (this._settings.appfolder.BLUR) { + this._appfolder_blur.enable(); + } else { + this._appfolder_blur.disable(); + } + }); + + // appfolder dialogs style changed + this._settings.appfolder.STYLE_DIALOGS_changed(() => { + if (this._settings.appfolder.BLUR) + this._appfolder_blur.blur_appfolders(); + }); + + + // ---------- PANEL ---------- + + // toggled on/off + this._settings.panel.BLUR_changed(() => { + if (this._settings.panel.BLUR) { + this._panel_blur.enable(); + } else { + this._panel_blur.disable(); + } + }); + + this._settings.COLOR_AND_NOISE_changed(() => { + // permits making sure that the blur is not washed out when disabling + // the other effects + if (this._settings.panel.BLUR) + this._panel_blur.invalidate_all_blur(); + }); + + // static blur toggled on/off + this._settings.panel.STATIC_BLUR_changed(() => { + if (this._settings.panel.BLUR) + this._panel_blur.update_all_blur_type(); + }); + + // panel blur's overview connection toggled on/off + this._settings.panel.UNBLUR_IN_OVERVIEW_changed(() => { + this._panel_blur.connect_to_windows_and_overview(); + }); + + // panel override background toggled on/off + this._settings.panel.OVERRIDE_BACKGROUND_changed(() => { + if (this._settings.panel.BLUR) + this._panel_blur.connect_to_windows_and_overview(); + }); + + // panel style changed + this._settings.panel.STYLE_PANEL_changed(() => { + if (this._settings.panel.BLUR) + this._panel_blur.connect_to_windows_and_overview(); + }); + + // panel background's dynamic overriding toggled on/off + this._settings.panel.OVERRIDE_BACKGROUND_DYNAMICALLY_changed(() => { + if (this._settings.panel.BLUR) + this._panel_blur.connect_to_windows_and_overview(); + }); + + + // ---------- DASH TO DOCK ---------- + + // toggled on/off + this._settings.dash_to_dock.BLUR_changed(() => { + if (this._settings.dash_to_dock.BLUR) { + this._dash_to_dock_blur.enable(); + } else { + this._dash_to_dock_blur.disable(); + } + }); + + // TODO implement static blur for dash + // static blur toggled on/off + this._settings.dash_to_dock.STATIC_BLUR_changed(() => { + //if (this._settings.dash_to_dock.BLUR) + // this._dash_to_dock_blur.change_blur_type(); + }); + + // dash-to-dock override background toggled on/off + this._settings.dash_to_dock.OVERRIDE_BACKGROUND_changed(() => { + if (this._settings.dash_to_dock.BLUR) + this._dash_to_dock_blur.update_background(); + }); + + // dash-to-dock style changed + this._settings.dash_to_dock.STYLE_DASH_TO_DOCK_changed(() => { + if (this._settings.dash_to_dock.BLUR) + this._dash_to_dock_blur.update_background(); + }); + + // dash-to-dock blur's overview connection toggled on/off + this._settings.dash_to_dock.UNBLUR_IN_OVERVIEW_changed(() => { + if (this._settings.dash_to_dock.BLUR) + this._dash_to_dock_blur.connect_to_overview(); + }); + + + // ---------- APPLICATIONS ---------- + + // toggled on/off + this._settings.applications.BLUR_changed(() => { + if (this._settings.applications.BLUR) { + this._applications_blur.enable(); + } else { + this._applications_blur.disable(); + } + }); + + // application opacity changed + this._settings.applications.OPACITY_changed(_ => { + if (this._settings.applications.BLUR) + this._applications_blur.set_opacity( + this._settings.applications.OPACITY + ); + }); + + // application blur-on-overview changed + this._settings.applications.BLUR_ON_OVERVIEW_changed(_ => { + if (this._settings.applications.BLUR) + this._applications_blur.connect_to_overview(); + }); + + // application enable-all changed + this._settings.applications.ENABLE_ALL_changed(_ => { + if (this._settings.applications.BLUR) + this._applications_blur.update_all_windows(); + }); + + // application whitelist changed + this._settings.applications.WHITELIST_changed(_ => { + if ( + this._settings.applications.BLUR + && !this._settings.applications.ENABLE_ALL + ) + this._applications_blur.update_all_windows(); + }); + + // application blacklist changed + this._settings.applications.BLACKLIST_changed(_ => { + if ( + this._settings.applications.BLUR + && this._settings.applications.ENABLE_ALL + ) + this._applications_blur.update_all_windows(); + }); + + + // ---------- LOCKSCREEN ---------- + + // toggled on/off + this._settings.lockscreen.BLUR_changed(() => { + if (this._settings.lockscreen.BLUR) { + this._lockscreen_blur.enable(); + } else { + this._lockscreen_blur.disable(); + } + }); + + + // ---------- WINDOW LIST ---------- + + // toggled on/off + this._settings.window_list.BLUR_changed(() => { + if (this._settings.window_list.BLUR) { + this._window_list_blur.enable(); + } else { + this._window_list_blur.disable(); + } + }); + + + // ---------- HIDETOPBAR ---------- + + // toggled on/off + this._settings.hidetopbar.COMPATIBILITY_changed(() => { + // no need to verify if it is enabled or not, it is done anyway + this._panel_blur.connect_to_windows_and_overview(); + }); + + + // ---------- DASH TO PANEL ---------- + + // toggled on/off + this._settings.dash_to_panel.BLUR_ORIGINAL_PANEL_changed(() => { + if (this._settings.panel.BLUR) + this._panel_blur.reset(); + }); + + + // ---------- SCREENSHOT ---------- + + // toggled on/off + this._settings.screenshot.BLUR_changed(() => { + if (this._settings.screenshot.BLUR) { + this._screenshot_blur.enable(); + } else { + this._screenshot_blur.disable(); + } + }); + } + + /// Select the component by its name and connect it to its preferences + /// changes for general values, sigma and brightness. + /// + /// Doing this in such a way is less accessible but prevents a lot of + /// boilerplate and headaches. + _connect_to_individual_settings(name) { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // general values switch is toggled + component_settings.CUSTOMIZE_changed(() => { + if (component_settings.CUSTOMIZE) { + component.set_sigma(component_settings.SIGMA); + component.set_brightness(component_settings.BRIGHTNESS); + component.set_color(component_settings.COLOR); + component.set_noise_amount(component_settings.NOISE_AMOUNT); + component.set_noise_lightness(component_settings.NOISE_LIGHTNESS); + } + else { + component.set_sigma(this._settings.SIGMA); + component.set_brightness(this._settings.BRIGHTNESS); + component.set_color(this._settings.COLOR); + component.set_noise_amount(this._settings.NOISE_AMOUNT); + component.set_noise_lightness(this._settings.NOISE_LIGHTNESS); + } + }); + + // sigma is changed + component_settings.SIGMA_changed(() => { + if (component_settings.CUSTOMIZE) + component.set_sigma(component_settings.SIGMA); + else + component.set_sigma(this._settings.SIGMA); + }); + + // brightness is changed + component_settings.BRIGHTNESS_changed(() => { + if (component_settings.CUSTOMIZE) + component.set_brightness(component_settings.BRIGHTNESS); + else + component.set_brightness(this._settings.BRIGHTNESS); + }); + + // color is changed + component_settings.COLOR_changed(() => { + if (component_settings.CUSTOMIZE) + component.set_color(component_settings.COLOR); + else + component.set_color(this._settings.COLOR); + }); + + // noise amount is changed + component_settings.NOISE_AMOUNT_changed(() => { + if (component_settings.CUSTOMIZE) + component.set_noise_amount(component_settings.NOISE_AMOUNT); + else + component.set_noise_amount(this._settings.NOISE_AMOUNT); + }); + + // noise lightness is changed + component_settings.NOISE_LIGHTNESS_changed(() => { + if (component_settings.CUSTOMIZE) + component.set_noise_lightness(component_settings.NOISE_LIGHTNESS); + else + component.set_noise_lightness(this._settings.NOISE_LIGHTNESS); + }); + } + + /// Update each component's sigma value + _update_sigma() { + INDEPENDENT_COMPONENTS.forEach(name => { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // update sigma accordingly + if (component_settings.CUSTOMIZE) { + component.set_sigma(component_settings.SIGMA); + } + else { + component.set_sigma(this._settings.SIGMA); + } + }); + } + + /// Update each component's brightness value + _update_brightness() { + INDEPENDENT_COMPONENTS.forEach(name => { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // update brightness accordingly + if (component_settings.CUSTOMIZE) + component.set_brightness(component_settings.BRIGHTNESS); + else + component.set_brightness(this._settings.BRIGHTNESS); + }); + } + + /// Update each component's color value + _update_color() { + INDEPENDENT_COMPONENTS.forEach(name => { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // update color accordingly + if (component_settings.CUSTOMIZE) + component.set_color(component_settings.COLOR); + else + component.set_color(this._settings.COLOR); + }); + } + + /// Update each component's noise amount value + _update_noise_amount() { + INDEPENDENT_COMPONENTS.forEach(name => { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // update color accordingly + if (component_settings.CUSTOMIZE) + component.set_noise_amount(component_settings.NOISE_AMOUNT); + else + component.set_noise_amount(this._settings.NOISE_AMOUNT); + }); + } + + /// Update each component's noise lightness value + _update_noise_lightness() { + INDEPENDENT_COMPONENTS.forEach(name => { + // get component and preferences needed + let component = this['_' + name + '_blur']; + let component_settings = this._settings[name]; + + // update color accordingly + if (component_settings.CUSTOMIZE) + component.set_noise_lightness(component_settings.NOISE_LIGHTNESS); + else + component.set_noise_lightness(this._settings.NOISE_LIGHTNESS); + }); + } + + _log(str) { + if (this._settings.DEBUG) + console.log(`[Blur my Shell > extension] ${str}`); + } +} diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/add-window-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/add-window-symbolic.svg new file mode 100644 index 0000000..bdab309 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/add-window-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/applications-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/applications-symbolic.svg new file mode 100644 index 0000000..87f307f --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/applications-symbolic.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg new file mode 100644 index 0000000..2eda98b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/dash-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/dash-symbolic.svg new file mode 100644 index 0000000..5a78b46 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/dash-symbolic.svg @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/general-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/general-symbolic.svg new file mode 100644 index 0000000..e11da07 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/general-symbolic.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/heart-filled-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/heart-filled-symbolic.svg new file mode 100644 index 0000000..8378c9c --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/heart-filled-symbolic.svg @@ -0,0 +1,40 @@ + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/other-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/other-symbolic.svg new file mode 100644 index 0000000..45e703e --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/other-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/overview-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/overview-symbolic.svg new file mode 100644 index 0000000..445f662 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/overview-symbolic.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/remove-window-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/remove-window-symbolic.svg new file mode 100644 index 0000000..a8da14f --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/remove-window-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/reset-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/reset-symbolic.svg new file mode 100644 index 0000000..e443de4 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/reset-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-mode-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-mode-symbolic.svg new file mode 100644 index 0000000..cad2da8 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-mode-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-window-symbolic.svg b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-window-symbolic.svg new file mode 100644 index 0000000..cad2da8 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/icons/hicolor/scalable/actions/select-window-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ar/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ar/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..4a999d6a07cd0453691f3d206c6ba8e6423db08e GIT binary patch literal 12893 zcmb7|dyHJweaBA&q{$LU0(q3sq_-jA5j^7u7>KcXnB^J#02>?%ZE|W2N}HzC1>1{_v%uPMh$0oWMXmBj6jkaSMGX?Fk|m9%p`Z!P$Rc_|&<6nQQ%7#ykN&^iKZpdF)-rd<@(L zegynea0q-EydC^&@MiG*cN-IdTfy&vC%_Mbdl(#me+0e<{0{g_;NO7n1z!i>2mU+A z6m!^`5laKJ@^IiLGT19Jug5R>G@GmdadD)?$J_lX{CSV7A7L?qJE-+>bcpr$V%rl_m%!50??}D2DAvP&}F9jvP0!sc` zkpJd-{`?NO1;q8{AHa*iAA=evWu^B!L9O$Ca0U2LQ0v|0aR;dJbr2QJ!{BQ0i=gy) z9>g`~yP(E>50tzgfMei~z-{1d7*)Ld8hACh06qZz47>px{*c?>9#H=JA$SvbF{F^Z zhrsK=2Dk})8q~Od1+M^CLRRte0Z`*>pw>&k3*TYPEV!2YKjGmj@b5sDHs@T7Z17^R z4L%Bf1N;g2bMSG3?NabD$aE8U2*d^Er=aZh1}M8;PVn*7Z1nhf@N(|I1WM0C{`pV9 zd$>R6SB$wA+yTnobD;D%3H~GT@qxgY-{Stif7SVE;^R)2Wdm zB=^JM)8O|(@v(!k#=$A@Yv9j7={J3u)8`u?TQny@sA&Eklz#sNO8@@>F_Ae7<0%e4 z4()hy2f1sYTs4x ztKdd(2lzu!e7&mb^5b0rAfYz_hJNAIDXjZZx|n+vzt&-ES!7jV_M%}He z>*%wdOR>I|OFCzt_xdlG#@aM{F1u{t(tgh3B6XOnxZcf`eeU;PFo)HDYxV-Xi;J+e zd=9{Cxx^(xz~-)B2e0E={JDk)pXY))1{zy_-T-EwyZItcoWpesm-Q!jGuI_tA=f=z z+2>ZiJjf+~pU1V1tIl;k*KJ%ka*1n%hW*G-_i`ytMz~0Q2KSnGaJ`R9A8|t;=x>JY zHy5Yk4%9I>ap6j{ol75a{Z9MMm|yewDUVRY<{I(yH~h1W;3xd|48&t`@3VSvbI^>t zy&xG4CgXNJnd}Dbq!;v(V5B+GsV?)ncvY_lJM0{DGha1c^LmXa7zt~SjCGQUc3pz? z#0d|&Q8TLbl1>n}?c;8(6GiQAgC*~~lhqcY`OQ7*D$0c%&Jv2wLGIQP5>ivT3c6B zuRr$3o0A~y^y1O5)_eW2uV)tJ#qCi$>V(bU^?kn^+<_;8X0$VEYRq^i8jU&;8><2mk~4daU^whHWSFgb$(Ta6ET}PoM4#f(H`u>o-2ZQ zG>Ch08YHM!3kkhs+%H`4u0-#+6^=o>jp*3uwWDs=+|mf!W6oDLBzyogS<92ZUXsdm_kYtGKm=qND-r(lPtcd#>TPDI*G1HGD4 z15BT2H_*F*EGaml^if#hQ7rWX~oWQ9B5;jEjHT zQP|05ctU9hqn)Ic2a;XL&cuUJC(E#z@apO2%N{;1wd|Qp#sux|c&Id#d!Qs@vF3Pz z)e~EmT*4SmB3Gp{j^$1%nPY`ZtcqrrGRWdRA2O2H4ttTgwQV`78d1~+|Fp1_wT7ok z@>a>nW)fHCSQ&++Tsg{Jv=HI9C7l`@$(6_5BN-jwHbrp~@8U|dn@l>7U^9&`pi*0oWzNmW%Acl}7k_CC36YYYq8jK5K@4aT@n(O)3vYGSyk2!(Y`#l*czSG zhV!nbmr)n(}3Qj=IJos(208LO5ja!lCqwT;zVY(pBVea*j= zw(WZ;*TC4n);4Bq5wd1$OplW1LrX5T&#FsAV@y!inqjv~FTi43JITX(IZi`j?)5GV zHc~Y1jk;0K{Vn$x?#V#gp@O(kwV^c?Y*T#>Vi?6P)RazD&9>SuA#I258>5HI2SadF&!10SLl zfPlQw=$ZSvB=8WcMK|A8M>SBt!$SYXiAdKy5Qz!%=K{G zZN$O)u<`IUpT6cM4*#t%ZdNhC4a{hdO<=0Z_9$#IzQfVG7OZdddgI-7D_1rWbdxVIW&6}P z8&XbKIo=H8_VpaU!wxRpaNqXZD%Ta`v@ed+m0R1jq)x}YF1T(a?hS24kFZ&xy=!&X z1?_Qr+ud-@^@01kp}k?ls^G?8RlwT~8&>mmwfnk;uWR)6ldD&)x?#gr!KIhF@9Q_L z){Lu)cdIq+YVOxwKlIs(Bbl}E4riKRZFNF-VtrO)=K(<4E8sK2+ruRjCsO227u)4Bd^(BGRL3evgs zM0&XYRDXYZxOyhD1}CHk(-Zvz{arzSpA(-R=|5?_$OGx&Abp<2clBrO3P%DII+h;i z=0JLUv0@q)NX_(U|8ewY;_mdM-$42j&tFQ9RNq9n+i3c&bUvGOibvCa>GUuXUPS%p z)8l#^qzf!&hYso}F~xHIS#~(f4((%St=W8IL4OZwvYjQHnP$D?ctAUUGCfosvgYRY zD^`hP>5Kh+a=}4d^LW52FIqRqW*CXxN^)RrVpl#Wt<$HiftdE3-Od~?L%)2c*YP9| zW-SBes?N=fdOKyxGwCU- z$?@P7bl?HP4x-NdD=&L(=b)KPwpSyp2lzeffOduT`!oF~-0E`@q+y!47afk_kiCIj znjr_R6z*9Ra?PQ^v+2>&k6%2}#^nn{AGL1S?^(q=Zv%sJFY<224V$-n+%L7#xgb5p zb6Ma`1!&i~Jk2wurI%VfxSZFSJ!h|2GT4(XJyKT9`=b;p#%_EtirLAGqrpHFI14RL z`y~Tqiet_L2c5we$3=F+aSbjm^X_yebJp;dACSHzil#AyNPB$nw~F)`F@F#H11u>N zlEkT~Hy}|Pv-;-o%q?6D(#1L#u`K-tG(n`Ey_lY4=ci2iZ8)d=V|kx3D2sPTx7~yt zp(3~YgrRhi8zShJf{09#ZPNzhfcOQ%>Z$%$ls)DYB`D9b115C03It`)X>gBIusXzw z*)h{th@nz>K>s{myauH-k$zdhFq=N7z}tt;`>LH!z|Az=90@~ zPA{0G#*R)PaW*|_oNgGI`rqGeEMJKqxzG@K3)YAWZVTKZ?b&p}C~{>fgBkZ@I|{Lu z%mOA$68iNqTVbErnI6*$MeX~tDYHW*Uiv`x)8(szOX)LNt=i4Xq;9rpql7jFZCI-` zc6rr`0(#mNZPYF+$SRuUpUhjQSADUdm*BBXF5=ke^d2Hgq4y~iIxbtdy&Y10%6lky z;N#2SNb;SlKMJwk&QW<)a{~r#A@3LVcx%nYSIsGEZk9KAJe|{WP^_Kt9BjcY5YtAO z-RUU^V2OXg2L?Clk@mBz5;4g3xUf95P+77?=C@O+Ty*=)tP+E2u6LonTi*G8Cl42G z4!v!OlJoG_e0oC0&RFg>_sz>8W|psJn1;8kti?`ROzWj-C(AUx zHCij_qFQn!3iXq4ns6(`zK$`r+SFunDoq~dDr)w%@ zrQEqe@pH7E%QCFiS6Ay2r?o68UODb42Dhb3*7ncboO#SVwB}!+rH*Jm3?w6vYID@H) zwMotp=z>vU_-$vz>DEnYY$f+(}!EtvFe^?6rakFX9!OLagCw0(M~s zQE0Dr=gY)FqW5^_P|}fg-nVV0)t|i-;-~BZvV3Y$t|C5>>l?GIYD$*?&M7<~vkW2% zdgJ?1OBkQEA$D3hs;r}@EnQR%4vWc4n?AxsX>5=43K9PdGdT5Syw8;*_wf7}MHI%$ zZoa=T>7SAG=EcmTJfBHlZ~-&W5$A7Fg!cpQ>~L&F>Sa*`mlDUKp(5GaVBHYb8)#>r zaVA!`y2OP#r7ysD+7fz2ZQr+p*hEoz`t~6&jKT6gIPJ`A)3Qz;)mqCN!`azqDQ?h^ zrlef7UC8CXZ{S3BMyHjfUNl$R>$=|sc)v?S*wF&yvQ2@O)8T&}`*Tzgg%xUL65C$F zPO-0WHM8AUWXoX)v!TmMb4E5S-VeM}hhGY2*LZW**LmDF_i6F#McF7Q<$ss)zsVSs z&wkS>dYJx`hMgY3ad@~qpC1cs({Q@bv)f=2%-0V~qTupJCo0Oca)gcps&`Hu<#c9K z*V%=7uLD{1s6{!%Blo)tUXgD|V^%{X_(lzf_`{+%XhsLu)-Sl!qlt2tJ&?J9_D2(C zjooCTB5E;-J>nquC2Oaw8AL}1S>WlcfMRrD!EFo8U0P}6lxZxXa`AS4ad|uPak`am&nIMK7Cf$mChk9k;r-7> z&Oe9Y>2q#SR!)@oeG4$~6AwC+_LpTyTR<|G=oI^e6I{xIgLPMSD!p7@s*PwTHOsrs zqBd^#^L^JtSs`C4uq0l=UaH~#=S`()TJ(%+K^A^0G&)4v$nEpXYu*Xigyl(0f}6n< zveVPrn{ta<(LAXsrRAr_ptKIIP!5;PINd9DI21w&YMkQM)>O{oZsVNMUHIX%{FeZq z6|-LvUwN5z2YPt@fat7;TxGr)8H*zlYoD@$xzGEq!qo@SY`*`*n~7s1{9c5Cyc}-= l?OZ{Rtm4OY7(R=6v5>WkP4W!NLvSGZl@wBar|AqZ2_6d6~sqHAg z!+F8DJmORoZRELwj;btp1HMmyWvOI_UK;HZX!oT9ew>#|L5Dm-x6)b2qLO};#BBpT z^7YmZIS-axrM!dEI43)bE<%9=0#{iIM`V3t9IyB!Q-(IM|MKl!18BYMV&?Z>z5}W2 zJSYAjO%@gG|MME|U^W0nLsFrpwA3X_I-?|pZx(GF;{H>xQ21-$e?GcbV#d^?ZEK!U z=EY_%9+P#+xNr+8p^Oy+(b_) ztTtCKiDpYQufR$xz1+IcWQ+eq=SAD1b_%_*DLh1(!=}SA<_K5iV&VAXwO)ZzC7CIH z=Uuv2VUI+cut;HJOtypa8EOev``(u>Z8ZAEdCHR))ZxyQ7~5-<3sJ5SX8J?P{~{bW z4h~2G$MM$Y)~$oTu#z<68%BH5Ong+bGEY0cQ7V*rjYo*qkzaq*zAah@bwMDHMBLJu z$))b4(CSDZZ!-^6;6b|PTpj_Gnu2<0KFC2frtAj8fR9riIKikb`yEciprhCO; zx~IW#BkrJKggYNO4SGKDGOIgSnC?wKaMeXv-msp!IPh+^J6;Ay(+{$cJc;G$+oCGu z!Toge$xx0)^NEwoP%_zqyp=jcEquGN_i1FM`1pQ4>2|Rj HWJg~CnG$az literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/bg/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/bg/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..f95a0e812e885da2b537a139519196eaa49755ed GIT binary patch literal 4362 zcmb7`-)|gO6~_lC&@NDZz4CC|mptHE+o>X@PJ^1JX{glvP-}iY&hB3C4w;=<&CJ@d zgpiXoNgJdF5C{nfsNspX#EBEfb~gV260@&>c*qO?2A}V}v$nT!gji|!?%X-|obUOb z?>RI7{?-e>4fuSO?|1nAjPE!3{)?|Z=5s+{SzlrO8|!se*)D!Q2wvxZ#M<*Oq3*Mg+JK}`>Up7m!e@(HXCQN(&VToh&0ij%lpHE~f4ZPDBprOZs(!)!1};&xQV zSz0vBVlOVR9t>*??>$mCb7#l zk8$2qin(@{WV@_Mjnaf_7qum$noe)k&eAf^5;)6N zOn*rJaVZMP!U;1@YmGToPoljYR_5dLYX$6vW4# zoow0W%duUlsT31l{zcZeCW%Tk0?vGjn2Gw|qqt>o27!8=*o&AKQYBtn5-MybS+{^@ zM8AwTDr-|$`_xhufaZ3PG@ zmC%tG(MUx*w}26Jde}rXSI?Gcb}-CoF6-lbrVxHU$XA&SCe@jW7CA-RyjHjq1XTAk z`ZKXzwQMsupJzX%CBk<*;SX&tLKYn6u)i8Q*}WMJQ(Inayzg=k&lKIb6TUU<7U5#H zU>fJoh40(tSO>$mA#K4No;&b&2m^q%{coWhtlpa>e_J8M*Riz)-dTtc?|t;^vZHjEX>aK zvZAz|=8A2p{>?1!&LSmcRLqKu-84RX)t%jQPsZKGnRm~e+M{5;IoCLe$m!8=adkji zC5o9HBvG6mW3)wiVaww`SbQfux@V{C7**k^w4HV0w7X!Aw&JpJ9vo2;BK&@_VA6rR zERG*OX8hmdsd?qNnLpO}ap7#~^L@A4#%_m=@uf+;O(L!<2EH$IcN_1c@ul%)uJ9sL{Ue{v z>3hxTJY7mV;~UkzYI}TB5PYUvZ0-19axkhM5acnzZpv-bRJYl4fJFw$jK&{pA-505 zSFqd?8Q46=<1RR;GeFw#AlLb`=BT_0FTWG+`!xlCxdJ0DH{*Of3b{#rAgA742cjw@ zY#>v~HmZjN--FvSmmMWmbxCDj~J6@dFR&7ta7@@a6lCiC%! z*lo%6A(sltInJekN0Ni{d8qmmEAignDJG4IJECcTm6~nqw1c+K#_xb zV@HIeL?mrCtT<#+6Gj;vgLr~xoH1hWo#fRvl2QGE(n!JZNhYV;8g5HFmG zu>)i~qxxSG)zxFjf(g8cS+&77D@t+HxFnxxD?}luQ$njYaLNr_)fp4OtKv@e$h`WT z%DgHGD(s$IMot;yk0$gf_uGJcI8|?tcZ(38Oj?ly#@A>HR%;3(5nQ_Os!1NI8l$tqxL}kUMyGWuYDe!GSZ5@!<71kssZl4x*!;e~7yyMuI>1P&Ia9+&7P0 zxP6{f)yE7gDPMVW#Fu*8X~zkTA3!)+adUkduErE}?aK<}w>7lHginIjphgeGzXa&V z_cOCj;OiRQ2hQl92O%oz9%AMq zO5id6cIT-dXcO6J=GDzFkRaXmtt9#cMUJIiH@(zFdWQX<&kLygR8jDV)`RLVE~M&m zeL|fYStCK->!?_hiAzVcJi%OaxT(4)FKLf+Q=^Nve!zTR(_sPE8T9i@KUxXypO=rE zFNN{Figit>pSE|9y@YhnF#bb^3t1;&kP%hTff%TczQ8-+4-wB8r?RZ3k3N+I1;+NAbBdf5rMe(T}f zwOR?h937mGG`c%~Oz83nU7;_z!iZ_{wD&mj)5|GDWpdqU1d^m>swQH}qfjqfp0Ul~ Ezi0~QTL1t6 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ca/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ca/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..2309a34cff58b035efb21cc836cf44c0107a2002 GIT binary patch literal 1402 zcmZWoJ&znk5FN0=)W}KKdT;9e_44U&5{xsjS7Afg z3$QO?ar^@N1NIp9BJANaN%9K(*YGdF{{$cJ{tEvY{O|BTz+QVcN&d$Aw^QAFKc7yL z*Rg&KPw;=jpN0Pi-oc-FE=h1LL3lf`Y~RIk@_e_|{^Qwh48~(mBi%R(mcmqs>sqUl zgR;iwG@L3=`1zo;rjZ1uNn^@*%36hGo(?Tl656XH@qwK9Mh8!|^ZIv_~6@Hme>U~;W8XP_>Pom&vEd97;&uW@ZneVA(!_^%UpDfshv#3 zi76#dX*H6;iQ1o7bO_&w^}m^1j%bnC&c)4@ERx$bm%x$?+oIYz*-2Z&LYc8$#OPq_ z|2A8?WHOO5tP|5zRa0>^XWa-bI-Y1|fmcfvDaeyOXFo@Dc4M4= zU7Utc`=ZyIS|4PbpSaVjJ1o8zX*dG#)T-p(@#6)RobKM(y><+~pKqjBtO;UJ@3ID< zAp4wni$Xd#HnTcHO#}FUZvG((i0; zQ#ai)bZ&?G+v%rS*H0}ocRL#BLf+rX`)M+mNF{2Y#^S|HY{@g!hOeu|LlaRFCD?}~ z2KO8Ed8Y&HL>X`(bRs(@1P5nFm>9GuI?U(k9-70B0982>?P-P9 o3mpGTxK53UI!qe8>j`LP7|TA{Bnm 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/de/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/de/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..2a7d711fda231b07e519c843dc8d2a9f811d4848 GIT binary patch literal 11858 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 A(*OVf literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/el/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/el/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..5825103ff91832a7d306235661b301fef2caf5cb GIT binary patch literal 15185 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/hu/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/hu/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..594115a5c02c9301bad37e75742f68c291cf1c8a GIT binary patch literal 11464 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 L&oki1hM5sYVPVvo?mOM(R97`s)eK`u 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/pt/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/pt/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..fb807053f467c5def38fd4841a791cff9421cdd3 GIT binary patch literal 11087 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ru/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ru/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..672f1924a1764d1ec427bd89fd77b269eeb862d2 GIT binary patch literal 14926 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sl/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sl/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..abb13d0c9f52c9c8f11313254ce1c67bdfecf331 GIT binary patch literal 11134 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sv/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/sv/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..b561cb0c35b4e0c70c7b4ef1880c761704644811 GIT binary patch literal 11310 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ta/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/ta/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..6854bc43596c18cb4e47011398042712d740d16f GIT binary patch literal 19778 zcmd6uYmjAERmTs6XG4$#2n0wxBtR0F+cOymgh?h$CX;DDPsXbjR;Z!t)3(}JW2_^N=gg;K%ss>v56KRw8R3n3JVMU{ny%i zpL1{DehiN)yXM?=&pvyv$AA6T+WTC>H_m_3gBgEQTrcLj_M9xc5BavI@WJ2m)3WSh z9q?$ZsJP=Vb)97x`+Wpj<>g5BZD8Y2>dXk0HO0l>ROtT!VZ!@~z0vBIW1# zI3qtVLdvh5d`SO3q}II&DL?N5gUo}886PeIDgdB|z~Kq{VZBYzrs=_OhAQsfOt#W#z*3;8F=+mIJtnq_~7oJM{Gc^#8+E&DoB^uHOv(NWNlLTvYdv3YM9NMJsrY{h zDPDXLskr8^gM-KqAcf=CkU)_AGxB2Ob0FJ4A$KA#M_#ap93x+kBsJMR$om07!D9Z-oW*Iu4iyv&vhwR!SyDt_(#fYZx}-MVbhABNJGl;V=}&tDRItCd z1o?^}#e=tTy_oCmVQeq*rQv%NP%V20*PAtPeco#J`gvzMUu?FgI*Yx$-RbB3PCn6E z=#GzgTs@ka%8%HZ?0UXxzSi|;%Y34kJUY|uEVQSjSR<}H@0G1`vft_E&9)8qCc9+4%N6()+1`o^rnYwPYWB96ArfYLMInNVX%7S4=1ddd^7*1& zDw+M+d|%O<6)*=hcc+p+?F?|^JZU3g9KCKm4<$2J{Yce zSEF}xu9$&#d+~9$-!6N-?1tH*J>$8uk_ZLR!k)8Tev4^+u_SqL8OcxCfnu(v7br4Y z9BXz2j#tyvQoERIPCCxz^fWaEr(j2*cYdsBEtHC87Qb3c1596Nx9~Gx>MWQWHR_r^ zplIidv(3rbe6qmJ5lo}P%6`A>$|g1fB^O2ZvfXcXODs+`dj&+!CtJ<=dFrcMPIU{q z3(9Z4WB=_p=Dpdn)!JqSHdjzubMp{+p!RyrnRauUvTFBHII++t_n4q&=UZi8My8wZ za)3mffWrWClbsgLCZMKts5n(b#iZ&vJn&hQop!(5X@Oa1F`ru?ekTM`Schn~Nt}^t zz?v*%H<%qYi*~08XzmMSz*oA0w-4^HQ1trcRCdEcuiu$#zPlvr9U4x*S@yDhMfWHl zy;=JZPmHBjHv7tng_&%hx36qpKo>MqvBa^(*|MD%vE!1Tc3E`eia;nuFx~CUr9v`8 zwvr5%-PmEH^6~Z73y)BjGP{+de4E2E4#~NCm3y^N z;Wu@MS;>ue`TJUCaSXwPR&(Xik;%--&|aj-8spnc2FMo{#+8f^DtYZOutqdWobV zXSY>d*Q)4t?W#iD&m#>x`y|~4290XXZlGl$Q`K%(vJtUYR&?eR)#ZdU*KE|rwxv#J z{XX>mfnBJEK7;{=f(>Y-Nk~|4*I~DgyRst0`}*1{G)__zIzzxnZQ(Rt&LhX53Mry+ zwHvfc5|4x3a=z2;lT1JFjAylz2H!2wa~$sa@g`-RR()AtERM=K|F!e5Tft+xxcg{L z+=;b~Qx3hh5Wd<>+H9w8q?Ptm4r`<}+wW~V+b`g={e;woA+^I{HD*;+~Mzi1B`tdHrauyWU~WJ z&M2uL8o6|Q*1kkFHYv)U{pvCva_HD)R7}TbcthXwEN6+t| zOT_OZw!PRt8n;X}?oz>b+PkbHHRhHYc2?h0EVRr1yJ(2o9vZjzW|~tC`b)2IsIw~{ zJFvfTXL+otg^hjeE4%WYJ9fURvE#LkD_@mg`I=oE;jq#K8n8V)L*?ymfj zTgUf~@1y({+WD>BW^cA#T#{dVbYjYej{3+RT1&Gv4oKIbp*^zzoQ*?eF}Eu}#H&y% zzjn6YpYQG3zJ0c%1NQi0IiZ8lc&9tF9f;K$d)rl8DW0LbezCn$y({k*W4GUO`;8SA zSB~!((*Yj(Hx4b$6QA0~_W4%PY+u70deJ58yAL0_sd05RPhs-L-MFzm*_q-{y(_65D~53a5}y1Me1zJ%LP+m}zTuAK70+{AnAo_U|({=>H7Gpj2P*VJ(P zrPY;Bv6>mSTnbUb=xJFweR6yw%d9%HdiqyaS3YhIgoQt%wYsODFui=dNN~f>Tm+_!gW*jUpU!S0))kBr|6sASkTB3O5=e z{s0++LFZt)wjO{0#~^8wBBe!+qzo%?-1A)5;n56#K<4CS%qYObc`ILH1}J&t?^CKn z!mHIk!LYE5GwvIpVTouMNx_NJczAz|T{P#u&WKb3pRuKp0WtlKKs|;4qy=6tpv^LZ zjz^o%-yk9u7Avw&z2`oOcEx*LRp9ntdx7F2^FId@#Z`^6P;S#B^n1lJRT-1P`(q(JvZ-|flqbwXpvY}H1aQqjmKOl%gCKRX z_M`De^iT=|CI~u8WSSmO3?lXrZ`7jK=bLe4T~+r*X5*`|A@JBUsSqDBH1A!{hPi9? zwD}CQ4Z3a>XIa5PG|>etm6u!C%e6c{c7>}apONggN1&1%fVl6x&YG$Vth;C+DwMK3 zF@z?`h;MqTT9lB$jaocQ69*s%Sfaop**N0#Q>DMduC9=dQ>h33id9`u0}b$iCNI7c z^+rIN!}Zpz*i^(@E-GhXOe-#-;B!uS5K-6hvA|S%!g4KH1Yfb1xkKcX2CpE?c$`{w zNQ{N3fdEb@pRNYYo?I58gG{BCuo)-=r!+r?;a5@!vO06P8G@_ydN7-YF>n%acekFg zb)91)q7(sy{;MvMS?8*H@FvKQOn80AhpobYlj!X6X*>Rg?iv!v1K+u!eaw52f&k_E>P9Q9=h&__I zs(|n-q4gvBzT@RB+dMT#fz3CBkc**RIYJmSiC<)4*|@_}up`SNpeYm0n1OLZ{eAny zJH-=er8BlaK2@#r$EQyUOddKow#4c!x)dUCto;BiYoP7#KvwwX52N6SXFfOir$fD& z5L99F-T86s5XLFKWoCnUGnTl0>y?8;*))=A8%raBi?C&G4ugvvDX=Zg4>qr z(Q3nAg8?E|bZpqT@<1)ezzSdlSCx!lx9ua=NvdndYXl5vA%DT``{jTbK(65u-$h@n zuslIuCzAU>+@Yl~8y#`GvbcSvtErkuRZ-$2YGSbZj=@T$a87WNq?5zj#GKmX#zQJ(?my<%l;BMjs#T{HT&lqtl zDRN6$>oCC6wWY_N(^p5Z}-nh5)^r= z4Qp>;(yJ8)GF~V%C}e2RU~LcC%m?XB7#0P!2um0Yp3uc-XxnymgWZ0oDQi zx}gX;6*$fI$D8OOuS0)3Q+sVn#YYb71>nsK#~4iKsI6ony*#lnpoBfJOTTLpq5>w~ zkr^9VAay`T<0#}r?`>rJp_n6c-V(s*Q0;pK1W3}^B;kQN7^Yj6LLX~RUP*!P$tGEramV3=>nE&Q+%KI|90h#w84v|_d1)uLas?--c_y(m8V_|#DtlTuhcTbfZWV%k6?l+(x`LnG#W>M#;89ZVtzDLaSnh0#oe>>wR7HM z#zidgP%xr%bczX;_gO!-m?AAgL6uhYkTkEzwP9BYi@t!TPZp};Ap2vA-2;<@cm&un zrkF9{7;$Q2RX71BY?KfSEdj9ZhC*1LW932}v0@L=vmT4+3I*J-=MIS2!F-T|P)v!z zCe)=k1+y-(f@hc&FJmXbaEgAAJYTT3f^Px|^)LwpWGAzqi}3{V)qWFI&eVmNzJ7~~ zJS3T6Zalu3YI#$u1uT)bYAz4MfyKnfh&m&?eJY_!ORzQ7&a6`U)^#%+!z6RD?5s_{Z<4LO;UOg-VI+IF76sImsjbdik z@;BCSQeY+t^nD`|NcqD*ydrZf0Nqrp@5AE}jf)`=ytiSdH&*EGNe?iiw_Z`6i|}fu zVz6k#6p{ujE6kk{A#&z|KI%4jqc~$Y;Ac={$kg_XSVZXBsIBnWYjk8yuf@KeT^x+N ziER+XRI8L&?pT1{0yJiU*a$Ra(fwODrEA%X6;ki`sUiX?9KbV%2gnZ`9yX#b_JNX6 z;1GJzctUSwa?d&AzqD+c+_l?Ty(&T*!wl8zzJ)lDEGQ$RD2oXl;Fq`0Hz9~(oYPlE0;@7%9 z;3LnxjVcgW=;7myeyRW{8UVrI7N4;x8Ey|mCNPR!5(7ORvU$o7t7{PoYz>iK^tg@T z_J>WJVJ$t&e>B9MD*LSFX5)UQ81Pxp>oanUA?2X4MGZt2&n1y*=*9JV_ZT?T*<*Q6 zOqdNJ`l25;Ui*cY)|2>V{PzDhgGIKSSZQgiC&)cjLZBv4s0nxqy;MZDDjAw5$e3h^ zOVh*dyl>v&z+-uKG6%I4CDclt1uPDmhNuTEBA-=UvEf=h@i^tY{!Gd-k62q(N{qXP z)7m{z0s$lRr**;@xMwnDkZ9v543NB5SvABQT(rRkfrg&?aRZ$osM@qo$4+iAmj?+k z3`b-v5p_nRax_^(8S)%=fdOEv((~^;V+ieVk|fnLc$Qu$OBf_=DLFD`1|oeD7+$~r ztDjCSb!So&7~Ui5F=LFJFV~E5i<(5JGZwB0%G5e%BLV{j5w`uU>RY=Vt7F`6b*pB{ z4QCcmO&uguFAvVb7d96H0FP6Kt8c{E4n?*x0E6|jh5wRF#wt)~Kahbwq!mUw>G$;os$QMR9}%U0$E(K$FgTdS$(O}N2P^5 zUuRj?vvT-#?!kbaw0JRiX{8_Myv(gX2G$Oq)JB&x$1ybH3PcDLpg+*@ViW`Z`k}yG zQQuDc4i0MzusA4KvaH7r3D2Uk2#M-?D+tT6s;hEC%x;~2Cv2;;KM%uk(i=uBNh=(- zD&HH1`a&pz;GSV0h9IfvQ@IX0x|Pup-dBE-=jBKgyp8kmcOwP>Jz%YK*uk%@fE8Pn zg;D?C_d|NIZzry;=&Auv1h^Ks0gxfjSQ)kF)E2Ti`Ei_77zc!3Rg7Cq9kzW(}uU^ZO0i2$JJxL+oVUd!S=n7H!&Z@Yr7)zi5#IK0sMTo{4eSDF@VN?W-8o%{4 zob$xDhKNu(8a2x*t;1Wdrn@gXG001IgE{sy50L`FsSz27Th;OH1`BR5inzXQgXtlQ#>rR` z|4-anH*gqL6I)!f~@{eGI=&_+8ycD2wN8645o0CUh_IPX8dJG^t*agm6a7LbWi5kt%rK zR(uywK-{xQpgid?c+>($s>dt+&jIVA?yn*!b140gQJFsr>_e*)z8Ku#7n)>=FQ+`W zEP&mc{D%V8S9QDvFAfmlEs%S7A*ihfp@z1jV=J@pMK3Bfum)ZoLo44>n2|k4F&5 Ij33Yb4+3(~3;+NC literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/tr/LC_MESSAGES/blur-my-shell.mo b/gnome/.local/share/extensions/blur-my-shell@aunetx/locale/tr/LC_MESSAGES/blur-my-shell.mo new file mode 100644 index 0000000000000000000000000000000000000000..55feb4f6526680422870861e259701158fd42477 GIT binary patch literal 11767 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 z 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 rB^BI1iX#iU9slGl=9K959Ywntnsj { + let list_box = prefs_group + .get_first_child() + .get_last_child() + .get_first_child(); + + let elements = []; + let i = 0; + let element = list_box.get_row_at_index(i); + while (element) { + elements.push(element); + i++; + element = list_box.get_row_at_index(i); + } + + return elements; +}; + + +export const Applications = GObject.registerClass({ + GTypeName: 'Applications', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/applications.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'blur', + 'customize', + 'opacity', + 'blur_on_overview', + 'enable_all', + 'whitelist', + 'add_window_whitelist', + 'blacklist', + 'add_window_blacklist' + ], +}, class Applications extends Adw.PreferencesPage { + constructor(preferences, preferences_window) { + super({}); + this._preferences_window = preferences_window; + + this.preferences = preferences; + + this.preferences.applications.settings.bind( + 'blur', this._blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.applications.settings.bind( + 'opacity', this._opacity, 'value', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.applications.settings.bind( + 'blur-on-overview', this._blur_on_overview, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.applications.settings.bind( + 'enable-all', this._enable_all, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._customize.connect_to(this.preferences, this.preferences.applications, false); + + // connect 'enable all' button to whitelist/blacklist visibility + this._enable_all.bind_property( + 'active', this._whitelist, 'visible', + GObject.BindingFlags.INVERT_BOOLEAN + ); + this._enable_all.bind_property( + 'active', this._blacklist, 'visible', + GObject.BindingFlags.DEFAULT + ); + + // make sure that blacklist / whitelist is correctly hidden + if (this._enable_all.active) + this._whitelist.visible = false; + this._blacklist.visible = !this._whitelist.visible; + + // listen to app row addition + this._add_window_whitelist.connect('clicked', + _ => this.add_to_whitelist() + ); + this._add_window_blacklist.connect('clicked', + _ => this.add_to_blacklist() + ); + + // add initial applications + this.add_widgets_from_lists(); + + this.preferences.connect('reset', _ => { + this.remove_all_widgets(); + this.add_widgets_from_lists(); + }); + } + + // A way to retriew the whitelist widgets. + get _whitelist_elements() { + return make_array(this._whitelist); + } + + // A way to retriew the blacklist widgets. + get _blacklist_elements() { + return make_array(this._blacklist); + } + + add_widgets_from_lists() { + this.preferences.applications.WHITELIST.forEach( + app_name => this.add_to_whitelist(app_name) + ); + + this.preferences.applications.BLACKLIST.forEach( + app_name => this.add_to_blacklist(app_name) + ); + + } + + close_all_expanded_rows() { + this._whitelist_elements.forEach( + element => element.set_expanded(false) + ); + this._blacklist_elements.forEach( + element => element.set_expanded(false) + ); + } + + remove_all_widgets() { + this._whitelist_elements.forEach( + element => this._whitelist.remove(element) + ); + this._blacklist_elements.forEach( + element => this._blacklist.remove(element) + ); + } + + add_to_whitelist(app_name = null) { + let window_row = new WindowRow('whitelist', this, app_name); + this._whitelist.add(window_row); + } + + add_to_blacklist(app_name = null) { + let window_row = new WindowRow('blacklist', this, app_name); + this._blacklist.add(window_row); + } + + update_whitelist_titles() { + let titles = this._whitelist_elements + .map(element => element._window_class.buffer.text) + .filter(title => title != ""); + + this.preferences.applications.WHITELIST = titles; + } + + update_blacklist_titles() { + let titles = this._blacklist_elements + .map(element => element._window_class.buffer.text) + .filter(title => title != ""); + + this.preferences.applications.BLACKLIST = titles; + } + + remove_from_whitelist(widget) { + this._whitelist.remove(widget); + this.update_whitelist_titles(); + } + + remove_from_blacklist(widget) { + this._blacklist.remove(widget); + this.update_blacklist_titles(); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/customize_row.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/customize_row.js new file mode 100644 index 0000000..0115966 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/customize_row.js @@ -0,0 +1,163 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; +import Gtk from 'gi://Gtk'; + + +/// Given a component (described by its preferences node), a gschema key and +/// a Gtk.ColorButton, binds everything transparently. +let bind_color = function (component, key, widget) { + let property_name = key.replaceAll('-', '_').toUpperCase(); + + let parse_color = _ => { + let [r, g, b, a] = component[property_name]; + let w = widget.rgba; + w.red = r; + w.green = g; + w.blue = b; + w.alpha = a; + widget.rgba = w; + }; + component.settings.connect('changed::' + key, parse_color); + + widget.connect('color-set', _ => { + let c = widget.rgba; + component[property_name] = [c.red, c.green, c.blue, c.alpha]; + }); + + parse_color(); +}; + +export const CustomizeRow = GObject.registerClass({ + GTypeName: 'CustomizeRow', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/customize-row.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'sigma', + 'brightness', + 'color', + 'color_row', + 'noise_amount', + 'noise_amount_row', + 'noise_lightness', + 'noise_lightness_row', + 'noise_color_notice' + ], +}, class CustomizeRow extends Adw.ExpanderRow { + /// Makes the required connections between the widgets and the preferences. + /// + /// This function may be bound to another object than CustomizeRow, if we + /// are using it for the General page; some things will then change (no + /// expansion row, and no notice) + /// + /// The color_and_noise parameter is either a boolean (true by default) or + /// a widget; and permits selecting weather or not we want to show the color + /// and noise buttons to the user. If it is a widget, it means we need to + /// dynamically update their visibility, according to the widget's state. + connect_to(settings, component_settings, color_and_noise = true) { + let s = component_settings.settings; + + // is not fired if in General page + if (this instanceof CustomizeRow) + // bind the customize button + s.bind( + 'customize', this, 'enable-expansion', + Gio.SettingsBindFlags.DEFAULT + ); + + // bind sigma and brightness + s.bind( + 'sigma', this._sigma, 'value', + Gio.SettingsBindFlags.DEFAULT + ); + s.bind( + 'brightness', this._brightness, 'value', + Gio.SettingsBindFlags.DEFAULT + ); + + // bind the color button + bind_color(component_settings, 'color', this._color); + + // bind noise sliders + s.bind( + 'noise-amount', this._noise_amount, 'value', + Gio.SettingsBindFlags.DEFAULT + ); + s.bind( + 'noise-lightness', this._noise_lightness, 'value', + Gio.SettingsBindFlags.DEFAULT + ); + + // color_and_noise is either a boolean or a widget, if true, or it is a + // widget, this will appropriately show the required preferences about + // setting the color and noise + if (color_and_noise) { + // if we gave the static_blur widget, we are dealing with the panel, + // and binding it to enable/disable the required components when + // switching between static and dynamic blur + if (color_and_noise instanceof Gtk.Switch) { + // bind its state to dynamically toggle the notice and rows + color_and_noise.bind_property( + 'active', this._color_row, 'visible', + GObject.BindingFlags.SYNC_CREATE + ); + color_and_noise.bind_property( + 'active', this._noise_amount_row, 'visible', + GObject.BindingFlags.SYNC_CREATE + ); + color_and_noise.bind_property( + 'active', this._noise_lightness_row, 'visible', + GObject.BindingFlags.SYNC_CREATE + ); + color_and_noise.bind_property( + 'active', this._noise_color_notice, 'visible', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // only way to get the correct state when first opening the + // window... + setTimeout(_ => { + let is_visible = color_and_noise.active; + this._color_row.visible = is_visible; + this._noise_amount_row.visible = is_visible; + this._noise_lightness_row.visible = is_visible; + this._noise_color_notice.visible = !is_visible; + }, 10); + } + + // if in General page, there is no notice at all + if (this instanceof CustomizeRow) { + // disable the notice + this._noise_color_notice.visible = false; + } + } else { + // enable the notice and disable color and noise preferences + this._color_row.visible = false; + this._noise_amount_row.visible = false; + this._noise_lightness_row.visible = false; + this._noise_color_notice.visible = true; + } + + // now we bind the color-and-noise preference to the sensitivity of the + // associated widgets, this will grey them out if the user choose not to + // have color and noise enabled + // note: I would love to bind to the visibility instead, but this part + // is already dirty enough, it would look like I obfuscate my code + // intentionally... (I am not) + settings.settings.bind( + 'color-and-noise', + this._color_row, 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + settings.settings.bind( + 'color-and-noise', + this._noise_amount_row, 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + settings.settings.bind( + 'color-and-noise', + this._noise_lightness_row, 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + }; +}); diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/dash.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/dash.js new file mode 100644 index 0000000..ef8d9ed --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/dash.js @@ -0,0 +1,43 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; + + +export const Dash = GObject.registerClass({ + GTypeName: 'Dash', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/dash.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'blur', + 'customize', + 'override_background', + 'style_dash_to_dock', + 'unblur_in_overview' + ], +}, class Dash extends Adw.PreferencesPage { + constructor(preferences) { + super({}); + + this.preferences = preferences; + + this.preferences.dash_to_dock.settings.bind( + 'blur', this._blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.dash_to_dock.settings.bind( + 'override-background', + this._override_background, 'enable-expansion', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.dash_to_dock.settings.bind( + 'style-dash-to-dock', this._style_dash_to_dock, 'selected', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.dash_to_dock.settings.bind( + 'unblur-in-overview', this._unblur_in_overview, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._customize.connect_to(this.preferences, this.preferences.dash_to_dock, false); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/general.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/general.js new file mode 100644 index 0000000..3067a2d --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/general.js @@ -0,0 +1,49 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; + +import { CustomizeRow } from './customize_row.js'; + + +export const General = GObject.registerClass({ + GTypeName: 'General', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/general.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'sigma', + 'brightness', + 'color', + 'color_row', + 'noise_amount', + 'noise_amount_row', + 'noise_lightness', + 'noise_lightness_row', + 'color_and_noise', + 'hack_level', + 'debug', + 'reset' + ], +}, class General extends Adw.PreferencesPage { + constructor(preferences) { + super({}); + + this.preferences = preferences; + + CustomizeRow.prototype.connect_to.call(this, preferences, preferences); + + this.preferences.settings.bind( + 'color-and-noise', this._color_and_noise, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.settings.bind( + 'hacks-level', this._hack_level, 'selected', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.settings.bind( + 'debug', this._debug, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._reset.connect('clicked', _ => this.preferences.reset()); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/menu.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/menu.js new file mode 100644 index 0000000..5ea2f3f --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/menu.js @@ -0,0 +1,67 @@ +import Gdk from 'gi://Gdk'; +import Gtk from 'gi://Gtk'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + + +export function addMenu(window) { + const builder = new Gtk.Builder(); + + // add a dummy page and remove it immediately, to access headerbar + builder.add_from_file(GLib.filename_from_uri(GLib.uri_resolve_relative(import.meta.url, '../ui/menu.ui', GLib.UriFlags.NONE))[0]); + let menu_util = builder.get_object('menu_util'); + window.add(menu_util); + try { + addMenuToHeader(window, builder); + } catch (error) { + // could not add menu... not so bad + } + window.remove(menu_util); +} + +function addMenuToHeader(window, builder) { + // a little hack to get to the headerbar + const page = builder.get_object('menu_util'); + const pages_stack = page.get_parent(); // AdwViewStack + const content_stack = pages_stack.get_parent().get_parent(); // GtkStack + const preferences = content_stack.get_parent(); // GtkBox + const headerbar = preferences.get_first_child(); // AdwHeaderBar + headerbar.pack_start(builder.get_object('info_menu')); + + // setup menu actions + const actionGroup = new Gio.SimpleActionGroup(); + window.insert_action_group('prefs', actionGroup); + + // a list of actions with their associated link + const actions = [ + { + name: 'open-bug-report', + link: 'https://github.com/aunetx/blur-my-shell/issues' + }, + { + name: 'open-readme', + link: 'https://github.com/aunetx/blur-my-shell' + }, + { + name: 'open-license', + link: 'https://github.com/aunetx/blur-my-shell/blob/master/LICENSE' + }, + { + name: 'donate-github', + link: 'https://github.com/sponsors/aunetx' + }, + { + name: 'donate-kofi', + link: 'https://ko-fi.com/aunetx' + }, + ]; + + actions.forEach(action => { + let act = new Gio.SimpleAction({ name: action.name }); + act.connect( + 'activate', + _ => Gtk.show_uri(window, action.link, Gdk.CURRENT_TIME) + ); + actionGroup.add_action(act); + }); +} diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/other.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/other.js new file mode 100644 index 0000000..d01f541 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/other.js @@ -0,0 +1,49 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; + + +export const Other = GObject.registerClass({ + GTypeName: 'Other', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/other.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'lockscreen_blur', + 'lockscreen_customize', + + 'screenshot_blur', + 'screenshot_customize', + + 'window_list_blur', + 'window_list_customize', + ], +}, class Overview extends Adw.PreferencesPage { + constructor(preferences) { + super({}); + + this.preferences = preferences; + + this.preferences.lockscreen.settings.bind( + 'blur', this._lockscreen_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._lockscreen_customize.connect_to(this.preferences, this.preferences.lockscreen); + + this.preferences.screenshot.settings.bind( + 'blur', this._screenshot_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._screenshot_customize.connect_to(this.preferences, this.preferences.screenshot); + + this.preferences.window_list.settings.bind( + 'blur', this._window_list_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._window_list_customize.connect_to( + this.preferences, this.preferences.window_list, false + ); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/overview.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/overview.js new file mode 100644 index 0000000..2560e3b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/overview.js @@ -0,0 +1,47 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; + + +export const Overview = GObject.registerClass({ + GTypeName: 'Overview', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/overview.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'overview_blur', + 'overview_customize', + 'overview_style_components', + + 'appfolder_blur', + 'appfolder_customize', + 'appfolder_style_dialogs' + ], +}, class Overview extends Adw.PreferencesPage { + constructor(preferences) { + super({}); + + this.preferences = preferences; + + this.preferences.overview.settings.bind( + 'blur', this._overview_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.overview.settings.bind( + 'style-components', this._overview_style_components, 'selected', + Gio.SettingsBindFlags.DEFAULT + ); + + this._overview_customize.connect_to(this.preferences, this.preferences.overview); + + this.preferences.appfolder.settings.bind( + 'blur', this._appfolder_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.appfolder.settings.bind( + 'style-dialogs', this._appfolder_style_dialogs, 'selected', + Gio.SettingsBindFlags.DEFAULT + ); + + this._appfolder_customize.connect_to(this.preferences, this.preferences.appfolder, false); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/panel.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/panel.js new file mode 100644 index 0000000..6d9f72b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/panel.js @@ -0,0 +1,65 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; + + +export const Panel = GObject.registerClass({ + GTypeName: 'Panel', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/panel.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'blur', + 'customize', + 'static_blur', + 'unblur_in_overview', + 'override_background', + 'style_panel', + 'override_background_dynamically', + 'hidetopbar_compatibility', + 'dtp_blur_original_panel' + ], +}, class Panel extends Adw.PreferencesPage { + constructor(preferences) { + super({}); + + this.preferences = preferences; + + this.preferences.panel.settings.bind( + 'blur', this._blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.panel.settings.bind( + 'static-blur', this._static_blur, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.panel.settings.bind( + 'unblur-in-overview', this._unblur_in_overview, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.panel.settings.bind( + 'override-background', + this._override_background, 'enable-expansion', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.panel.settings.bind( + 'style-panel', this._style_panel, 'selected', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.panel.settings.bind( + 'override-background-dynamically', + this._override_background_dynamically, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + this._customize.connect_to(this.preferences, this.preferences.panel, this._static_blur); + + this.preferences.hidetopbar.settings.bind( + 'compatibility', this._hidetopbar_compatibility, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + this.preferences.dash_to_panel.settings.bind( + 'blur-original-panel', this._dtp_blur_original_panel, 'active', + Gio.SettingsBindFlags.DEFAULT + ); + } +}); \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/window_row.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/window_row.js new file mode 100644 index 0000000..c9786c9 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/preferences/window_row.js @@ -0,0 +1,109 @@ +import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gio from 'gi://Gio'; +import Gtk from 'gi://Gtk'; + +import { pick, on_picking, on_picked } from '../dbus/client.js'; + + + +export const WindowRow = GObject.registerClass({ + GTypeName: 'WindowRow', + Template: GLib.uri_resolve_relative(import.meta.url, '../ui/window-row.ui', GLib.UriFlags.NONE), + InternalChildren: [ + 'window_picker', + 'window_class', + 'picking_failure_toast' + ], +}, class WindowRow extends Adw.ExpanderRow { + constructor(list, app_page, app_name) { + super({}); + this._list = list; + this._app_page = app_page; + + // add a 'remove' button before the text + let action_row = this.child.get_first_child().get_first_child(); + let remove_button = new Gtk.Button({ + 'icon-name': 'remove-window-symbolic', + 'width-request': 38, + 'height-request': 38, + 'margin-top': 6, + 'margin-bottom': 6, + }); + remove_button.add_css_class('circular'); + remove_button.add_css_class('flat'); + action_row.add_prefix(remove_button); + + // connect the button to the whitelist / blacklist removal + remove_button.connect('clicked', _ => this._remove_row()); + + // bind row title to text buffer + this._window_class.buffer.bind_property( + 'text', this, 'title', + Gio.SettingsBindFlags.BIDIRECTIONNAL + ); + + // set application name if it exists, or open the revealer and pick one + if (app_name) + this._window_class.buffer.text = app_name; + else { + app_page.close_all_expanded_rows(); + this.set_expanded(true); + this._do_pick_window(true); + } + + // pick a window when the picker button is clicked + this._window_picker.connect('clicked', _ => this._do_pick_window()); + + // update list on text buffer change + this._window_class.connect('changed', + _ => this._update_rows_titles() + ); + } + + _remove_row() { + this._app_page["remove_from_" + this._list](this); + } + + _update_rows_titles() { + this._app_page["update_" + this._list + "_titles"](this); + } + + _do_pick_window(remove_if_failed = false) { + // a mechanism to know if the extension is listening correcly + let has_responded = false; + let should_take_answer = true; + setTimeout(_ => { + if (!has_responded) { + // show toast about failure + this._app_page._preferences_window.add_toast( + this._picking_failure_toast + ); + + // prevent title from changing with later picks + should_take_answer = false; + + // remove row if asked + if (remove_if_failed) + this._remove_row(); + } + }, 15); + + on_picking(_ => + has_responded = true + ); + + on_picked(wm_class => { + if (should_take_answer) { + if (wm_class == 'window-not-found') { + console.warn("Can't pick window from here"); + return; + } + this._window_class.buffer.text = wm_class; + } + }); + + pick(); + } +}); diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/prefs.js b/gnome/.local/share/extensions/blur-my-shell@aunetx/prefs.js new file mode 100644 index 0000000..df622ed --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/prefs.js @@ -0,0 +1,41 @@ +import Gdk from 'gi://Gdk'; +import Gtk from 'gi://Gtk'; +import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +import { Settings } from './conveniences/settings.js'; +import { Keys } from './conveniences/keys.js'; + +import { addMenu } from './preferences/menu.js'; +import { General } from './preferences/general.js'; +import { Panel } from './preferences/panel.js'; +import { Overview } from './preferences/overview.js'; +import { Dash } from './preferences/dash.js'; +import { Applications } from './preferences/applications.js'; +import { Other } from './preferences/other.js'; + + +export default class BlurMyShellPreferences extends ExtensionPreferences { + constructor(metadata) { + super(metadata); + + // load the icon theme + let iconPath = this.dir.get_child("icons").get_path(); + let iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()); + iconTheme.add_search_path(iconPath); + } + + fillPreferencesWindow(window) { + addMenu(window); + + const preferences = new Settings(Keys, this.getSettings()); + + window.add(new General(preferences)); + window.add(new Panel(preferences)); + window.add(new Overview(preferences)); + window.add(new Dash(preferences)); + window.add(new Applications(preferences, window)); + window.add(new Other(preferences)); + + window.search_enabled = true; + } +} diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/schemas/gschemas.compiled b/gnome/.local/share/extensions/blur-my-shell@aunetx/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..c5aa01a980b3f4ba7164801abb4ec56fc8dce54f GIT binary patch literal 8160 zcmds+e{5A}9md}}y1{^f9bnmDGPA|oxZ8GsOWlU63DZttHvS-uKbD=_d+j~p_TFpn zZK3-i{evcr62%F#DvgcPjVLBnF=CucNnkdaBwgZSjWaSu!(XNvrWnlh`Mmdj?+>JS z%3o$Dd3c`tzsS%UnWixe?=#6QGBU>tWYQO=l}ja;&e}{?5Pw1g z#Qjf+kFR%bu<_wz#LwK~+}(=1TXmlsoV(P1X&>>AZ**?6;$z~Yw>g&^=xgoE=K52u zg>)*DX-(}drm}@Fmo2m=GJ^y0{^5A(Kk`8~l~D&87CmC!!8`yBtK%BX1@mCAxzLcg zi9NigyT4FP~aM9Q}61^Ws5a=o6n@O&r~*_=tE+D6HGQg*bY-;^X2; z!NwiUxWlu;_RZT?(H>1dbK(XCPT%q6=ZK@56mJ%96kdDy+ug*`s}ye+-!Du*`MKrH zE1G_~#E%PKf9}u|BCnSyKB6C^!dO223~}@_#Se-P3oqRDDyMmUqvDhLaZK2}Vc$Q9 zqint77?g(sWE-2xtF zZqRu6QE^7VtHjZGH7_0%@Fa0Go*WS$5XeE|X!;))PYUERaWuI+C4OEY*NCHOKO_Dt zx?B^{u%=fy__yht2P z|0l$!gtng?=p&BC({tkA36=wBJa=BaRtpbaq&=GWChP@|vCh{&OB~I8o)w=LUVr+T9}!3MTo=V{now#1 zaWv!BDc&J8HQe(WaWwt(h z@E2Ag2k^dLg(`l;Yr1`h@9MHzza!sI^+erJGAN|u#auk8I$L_M$ox1fT5Wx`@c-}EftrFAeSOl!7E@TPt}IljRQJ+)Cn)Cfi9nMZ`T46S>4t1+ zUc~u!iq5tbmw}0V{qn0wpi`y9jEo~RfuO&L#4N8a*g+^Jz+MP+Y`@(g`ziN7_2_%XruL}Z z`*HCE@$y~?&HWq|e_yb@6578Dh))TLuk8F8{h(<-D`vl)%LlzSvwznAlWz-^L-Hh* z4HDYyXiwxDUvWRgy2U!B*9t#xysK%c_v;ouWqvuBb1k1Pkx|~0-2`=XXM*f*y&=Sc zLd#;xdF4HEp-k8PTTXDD=y_4=VKT#Se@gooWIT7-4y=A(ReW9b!v=0?ckdK{6^z`m~SlXr>Dd}7R<9~ zJUcDEgf8a_nq2u8tQV2ekZ92lvq7e2UwrNBdfm-40bDTpv)8Qyi{C5qZOP}Vcv!H# zBAVPD6CV<6uZSjx$HfzZ?G@4Foo^9 z4`}9LgSbPmy&{^N?-KKE%yJ;Q|7!-2(Z5Q5z3KOft>#7Eiy3bhqlVc*{@7YZ-r8D5 zKHEBG%zUExM#+4-w>j4$F#p8S%zuY?yMVulqw!alc&A|biKd?(@lyi6qdgkmO^S~R z_?I{u|4xg4De!%dIGXQ!XT^UK8otoiL>x{0qS)yhD&N_NquCFw7OxfX9dR_i+a$hI z;Cmf$H1(xJyiFhvh@-jxE^#+nUgi5C8ehYF!;QQqM0me1RdUs9OA1{ndS29e!LJK- zJDu8n&O#}}ld-N|`BR?mtMqZP_CkIgD1CRVd^+>p`1)G+5k%)-;lrkHSM$Nf_6K;E zHHU-sh4qHDoHYmkvKHZId>wh+seMP*lhUspmMfMoT-(04n5X$pk0++Z7X_=mXuP{r z-|p84R(sLpMzeUMV6_)b4#dP;1*^Si`tKBX308a2_IdHw&}EOJ@#qop z3BhVFnjDxB^LK_Cn=Qg;)j_;k`Nc$-VToFezSDoSq1)EzmbK50O3wVn5Xgsx7ykEd0#L@JBQ2bK?PZ3AssTuLF1oJqW z_GiWG^tYyE7asgI?a{pR$HeUd-Y1U6`<>z)0^TQ%#{1o3{&r<4ZuwnT8q{9HmuS(y zjF07@RMaZBG+O%i%^fREkDc?WJjrBvTx00Q`w?k6?vxWzUB&l5~EU_M0qnjtl_&*eVEe;j!9|KEodPh7Q3*VXCr`W^ + + + + + + 30 + Global sigma (gaussian blur radius) to use + + + + 0.6 + Global brightness to use + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + true + Boolean, controls wether or not the color and noise effects are in use globally + + + + 1 + Level of hacks to use (from 0 to 3, 3 disabling clipped redraws entirely) + + + + false + Boolean, set to true to activate debug mode (more verbose journalctl logs) + + + + + + + + + + + + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + 1 + Enum to select the style of the components in overview (0 not styled, 1 light, 2 dark, 3 transparent) + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + 1 + Enum to select the style of the appfolder dialogs (0 not styled, 1 transparent, 2 light, 3 dark) + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + true + Boolean, whether to use a static or dynamic blur for this component + + + + true + Boolean, whether to disable blur from this component when opening the overview or not + + + + true + Boolean, whether to override the background or not + + + + 0 + Enum to select the style of the panel (0 transparent, 1 light, 2 dark, 3 contrasted) + + + + false + Boolean, whether to disable blur from this component when a window is close to the panel + + + + + + + + false + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + true + Boolean, whether to use static or dynamic blur for this component + + + + true + Boolean, whether to override the background or not + + + + 1 + Enum to select the style of dash to dock (0 transparent, 1 light, 2 dark) + + + + false + Boolean, whether to disable blur from this component when opening the overview or not + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + true + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 1. + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + 230 + Opacity of the window actor on top of the blur effect + + + + false + Wether or not to blur applications on the overview + + + + false + Wether or not to blur all applications by default + + + + [] + List of applications to blur + + + + ["Plank"] + List of applications not to blur + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + + + + + true + Boolean, whether to blur activate the blur for this component or not + + + + false + Boolean, whether to customize the blur effect sigma/brightness or use general values + + + + 30 + Sigma (gaussian blur radius) to use for the blur effect + + + + 0.6 + Brightness to use for the blur effect + + + + (0.,0.,0.,0.) + Color to mix with the blur effect + + + + 0. + Amount of noise to add to the blur effect + + + + 0. + Lightness of the noise added to the blur effect + + + + + + + + false + Boolean, whether to try compatibility with hidetopbar@mathieu.bidon.ca or not + + + + + + + + true + Boolean, whether to blur the original panel (if option selected) with Dash to Panel + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/stylesheet.css b/gnome/.local/share/extensions/blur-my-shell@aunetx/stylesheet.css new file mode 100644 index 0000000..91cfb1f --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/stylesheet.css @@ -0,0 +1,334 @@ +/*** PANEL ***/ + +/* +* `.transparent-panel` +*/ +#panel.transparent-panel { + background: transparent; + transition-duration: 500ms; +} + +/* +* `.dark-panel` +*/ +#panel.dark-panel { + background-color: rgba(100, 100, 100, 0.35); + box-shadow: none; + transition-duration: 500ms; +} + +/* +* `.light-panel` +*/ +#panel.light-panel { + background-color: rgba(200, 200, 200, 0.2); + box-shadow: none; + transition-duration: 500ms; +} + +/* +* `.contrasted-panel` +*/ +#panel.contrasted-panel { + background-color: transparent; + box-shadow: none; + transition-duration: 500ms; +} + +.contrasted-panel .panel-button, +.contrasted-panel .clock, +.contrasted-panel .clock-display StIcon { + color: #ffffff; + border-radius: 14px; + border: 3px solid transparent; + background-color: rgba(0, 0, 0, 0.8); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03); +} + + +.contrasted-panel .clock-display StIcon { + padding: 8px; + margin: 0; +} + +.contrasted-panel .panel-button:hover, +.contrasted-panel .panel-button:hover .clock { + color: #ffffff; + background-color: rgba(0, 0, 0, 0.8); +} + +.contrasted-panel .clock-display { + background-color: transparent !important; + box-shadow: none !important; + border: none !important; +} + +.contrasted-panel .clock { + margin: 0 !important; +} + + +/*** DASH ***/ + +/* +* `.transparent-dash` +*/ +.transparent-dash .dash-background { + background-color: transparent !important; +} + +/* +* `.light-dash` +*/ +.light-dash .dash-background { + background-color: rgba(200, 200, 200, 0.2) !important; +} + +/* +* `.dark-dash` +*/ +.dark-dash .dash-background { + background-color: rgba(100, 100, 100, 0.35) !important; +} + + +/*** OVERVIEW ***/ + +/* +* Add transparency to the workspace animation (between workspaces) +*/ +.blurred-overview .workspace-animation { + background-color: transparent !important; +} + + +/* +* `.overview-components-transparent` +*/ + +.overview-components-transparent .workspace-thumbnail { + background-color: rgba(0, 0, 0, 0); + border: 1px solid rgba(100, 100, 100, 0.35); +} + +.overview-components-transparent .search-entry { + color: white; + background-color: rgba(0, 0, 0, 0); + border-color: transparent; + box-shadow: none; +} + +.overview-components-transparent .search-entry .search-entry-icon { + color: rgba(255, 255, 255, 0.65); +} + +.overview-components-transparent .search-section-content { + border: none; + box-shadow: none; +} + +.overview-components-transparent .search-section-content, +.overview-components-transparent .app-folder .overview-icon { + background-color: rgba(0, 0, 0, 0); +} + +/* prevents the extension from interfering with Just Perfection */ +.overview-components-transparent.just-perfection .search-section-content { + background-color: transparent; +} + +.overview-components-transparent .app-folder .overview-icon { + border-color: transparent; +} + +.overview-components-transparent .app-folder:hover .overview-icon, +.overview-components-transparent .app-folder:focus .overview-icon { + background-color: rgba(230, 230, 230, 0.08); +} + +.overview-components-transparent .app-folder:active .overview-icon, +.overview-components-transparent .app-folder:focus:hover .overview-icon, +.overview-components-transparent .app-folder:drop .overview-icon { + background-color: rgba(230, 230, 230, 0.12); +} + +.overview-components-transparent .app-folder:focus:active .overview-icon { + background-color: rgba(230, 230, 230, 0.15); +} + +/* this shouldn't apply to Dash to Dock */ +.overview-components-transparent StBoxLayout>StWidget>#dash>.dash-background { + background-color: rgba(0, 0, 0, 0); +} + +/* +* `.overview-components-light` +*/ + +.overview-components-light .workspace-thumbnail { + background-color: rgba(200, 200, 200, 0.2); +} + +.overview-components-light .search-entry { + color: white; + background-color: rgba(200, 200, 200, 0.2); + border-color: transparent; + box-shadow: none; +} + +.overview-components-light .search-entry .search-entry-icon { + color: rgba(255, 255, 255, 0.65); +} + +.overview-components-light .search-section-content { + border: none; + box-shadow: none; +} + +.overview-components-light .search-section-content, +.overview-components-light .app-folder .overview-icon { + background-color: rgba(200, 200, 200, 0.2); +} + +/* prevents the extension from interfering with Just Perfection */ +.overview-components-light.just-perfection .search-section-content { + background-color: transparent; +} + +.overview-components-light .app-folder .overview-icon { + border-color: transparent; +} + +.overview-components-light .app-folder:hover .overview-icon, +.overview-components-light .app-folder:focus .overview-icon { + background-color: rgba(230, 230, 230, 0.2); +} + +.overview-components-light .app-folder:active .overview-icon, +.overview-components-light .app-folder:focus:hover .overview-icon, +.overview-components-light .app-folder:drop .overview-icon { + background-color: rgba(230, 230, 230, 0.25); +} + +.overview-components-light .app-folder:focus:active .overview-icon { + background-color: rgba(230, 230, 230, 0.3); +} + +/* this shouldn't apply to Dash to Dock */ +.overview-components-light StBoxLayout>StWidget>#dash>.dash-background { + background-color: rgba(200, 200, 200, 0.2); +} + + +/* +* `.overview-components-dark` +*/ + +.overview-components-dark .workspace-thumbnail { + background-color: rgba(100, 100, 100, 0.35); +} + +.overview-components-dark .search-entry { + color: white; + background-color: rgba(100, 100, 100, 0.35); + border-color: transparent; + box-shadow: none; +} + +.overview-components-dark .search-entry .search-entry-icon { + color: rgba(255, 255, 255, 0.65); +} + +.overview-components-dark .search-section-content { + border: none; + box-shadow: none; +} + +.overview-components-dark .search-section-content, +.overview-components-dark .app-folder .overview-icon { + background-color: rgba(100, 100, 100, 0.35); +} + +/* prevents the extension from interfering with Just Perfection */ +.overview-components-dark.just-perfection .search-section-content { + background-color: transparent; +} + +.overview-components-dark .app-folder .overview-icon { + border-color: transparent; +} + +.overview-components-dark .app-folder:hover .overview-icon, +.overview-components-dark .app-folder:focus .overview-icon { + background-color: rgba(120, 120, 120, 0.35); +} + +.overview-components-dark .app-folder:active .overview-icon, +.overview-components-dark .app-folder:focus:hover .overview-icon, +.overview-components-dark .app-folder:drop .overview-icon { + background-color: rgba(120, 120, 120, 0.4); +} + +.overview-components-dark .app-folder:focus:active .overview-icon { + background-color: rgba(120, 120, 120, 0.45); +} + +/* this shouldn't apply to Dash to Dock */ +.overview-components-dark StBoxLayout>StWidget>#dash>.dash-background { + background-color: rgba(100, 100, 100, 0.35); +} + + +/*** APPFOLDER DIALOG ***/ + +/* +* `.appfolder-dialogs-transparent` +*/ + +.appfolder-dialogs-transparent { + background-color: rgba(0, 0, 0, 0); + border: none; + box-shadow: none; +} + +.appfolder-dialogs-transparent .folder-name-entry { + color: white; + background-color: rgba(0, 0, 0, 0); + border: 0; + box-shadow: none; +} + +/* +* `.appfolder-dialogs-light` +*/ + +.appfolder-dialogs-light { + background-color: rgba(200, 200, 200, 0.2); + border: none; + box-shadow: none; +} + +.appfolder-dialogs-light .folder-name-entry { + color: white; + background-color: rgba(200, 200, 200, 0.2); + border: 0; + box-shadow: none; +} + + +/* +* `.appfolder-dialogs-dark` +*/ + +.appfolder-dialogs-dark { + background-color: rgba(100, 100, 100, 0.35); + border: none; + box-shadow: none; +} + +.appfolder-dialogs-dark .folder-name-entry { + color: white; + background-color: rgba(100, 100, 100, 0.35); + border: 0; + box-shadow: none; +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/applications.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/applications.ui new file mode 100644 index 0000000..0b4b83c --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/applications.ui @@ -0,0 +1,163 @@ + + + + + + 25 + 255 + 1 + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/customize-row.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/customize-row.ui new file mode 100644 index 0000000..313bc48 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/customize-row.ui @@ -0,0 +1,143 @@ + + + + + + 0 + 200 + 1 + + + + 0.0 + 1.0 + 0.01 + + + + 0.0 + 1.0 + 0.01 + + + + 0.0 + 2.0 + 0.01 + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/dash.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/dash.ui new file mode 100644 index 0000000..5eac21b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/dash.ui @@ -0,0 +1,73 @@ + + + + + + + Transparent + Light + Dark + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/general.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/general.ui new file mode 100644 index 0000000..73c6b29 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/general.ui @@ -0,0 +1,234 @@ + + + + + + 0 + 200 + 1 + + + + 0.0 + 1.0 + 0.01 + + + + 0.0 + 1.0 + 0.01 + + + + 0.0 + 2.0 + 0.01 + + + + + High performances + Default + High quality + No artifact + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/menu.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/menu.ui new file mode 100644 index 0000000..e7b9210 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/menu.ui @@ -0,0 +1,37 @@ + + + +
+ + Project page + prefs.open-readme + + + Report a Bug + prefs.open-bug-report + + + License + prefs.open-license + + + Donate + + GitHub + prefs.donate-github + + + Ko-fi + prefs.donate-kofi + + +
+
+ + + info_menu_model + heart-filled-symbolic + + + +
\ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/other.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/other.ui new file mode 100644 index 0000000..d1d24f8 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/other.ui @@ -0,0 +1,62 @@ + + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/overview.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/overview.ui new file mode 100644 index 0000000..191c70b --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/overview.ui @@ -0,0 +1,94 @@ + + + + + + + Do not style + Light + Dark + Transparent + + + + + + Do not style + Transparent + Light + Dark + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/panel.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/panel.ui new file mode 100644 index 0000000..41d9cd1 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/panel.ui @@ -0,0 +1,141 @@ + + + + + + + Transparent + Light + Dark + Contrasted + + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/window-row.ui b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/window-row.ui new file mode 100644 index 0000000..c4d8201 --- /dev/null +++ b/gnome/.local/share/extensions/blur-my-shell@aunetx/ui/window-row.ui @@ -0,0 +1,43 @@ + + + + + + Could not pick window, make sure that the extension is enabled. + + \ No newline at end of file diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.js new file mode 100644 index 0000000..583ad59 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.js @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +var PACKAGE_VERSION = 56; +var PACKAGE_URL = 'https://github.com/GSConnect/gnome-shell-extension-gsconnect'; +var PACKAGE_BUGREPORT = 'https://github.com/GSConnect/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/local/lib/x86_64-linux-gnu'; + +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'; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.mjs b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.mjs new file mode 100644 index 0000000..7295c97 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/config.mjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import GLib from 'gi://GLib'; + +const [filename] = GLib.filename_from_uri(import.meta.url); +const dirname = GLib.path_get_dirname(filename); +imports.searchPath.unshift(dirname); + +const Config = imports.config; +export default Config; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/extension.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/extension.js new file mode 100644 index 0000000..53ecd5f --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/extension.js @@ -0,0 +1,407 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js'; + +// Bootstrap +import { + Extension, + gettext as _, + ngettext +} from 'resource:///org/gnome/shell/extensions/extension.js'; + +import Config from './config.mjs'; +import * as Clipboard from './shell/clipboard.js'; +import * as Device from './shell/device.js'; +import * as Keybindings from './shell/keybindings.js'; +import * as Notification from './shell/notification.js'; +import * as Input from './shell/input.js'; +import * as Utils from './shell/utils.js'; +import Remote from './utils/remote.mjs'; +import setup from './utils/setup.mjs'; + +const QuickSettingsMenu = Main.panel.statusArea.quickSettings; + + +/** + * A System Indicator used as the hub for spawning device indicators and + * indicating that the extension is active when there are none. + */ +const ServiceToggle = GObject.registerClass({ + GTypeName: 'GSConnectServiceIndicator', +}, class ServiceToggle extends QuickSettings.QuickMenuToggle { + + _init() { + super._init({ + title: 'GSConnect', + toggleMode: true, + }); + + this.set({iconName: 'org.gnome.Shell.Extensions.GSConnect-symbolic'}); + + // Set QuickMenuToggle header. + this.menu.setHeader('org.gnome.Shell.Extensions.GSConnect-symbolic', 'GSConnect', + _('Sync between your devices')); + + 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/', + }); + + // Bind the toggle to enabled key + this.settings.bind('enabled', + this, 'checked', + Gio.SettingsBindFlags.DEFAULT); + + 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 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.menu.addMenuItem(this.deviceSection); + + // Service Menu -> Separator + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Service Menu -> "Mobile Settings" + this.menu.addSettingsAction( + _('Mobile Settings'), + 'org.gnome.Shell.Extensions.GSConnect.Preferences.desktop'); + + // 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'); + } + } + + _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 + serviceIndicator._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; + } + + // Set subtitle on Quick Settings tile + if (available.length === 1) { + this.subtitle = available[0].name; + } else if (available.length > 1) { + // TRANSLATORS: %d is the number of devices connected + this.subtitle = ngettext( + '%d Connected', + '%d Connected', + available.length + ).format(available.length); + } else { + this.subtitle = 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 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); + + if (!this.settings.get_boolean('keep-alive-when-locked')) + this.service.stop(); + 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.menu.destroy(); + + super.destroy(); + } +}); + +const ServiceIndicator = GObject.registerClass( +class ServiceIndicator extends QuickSettings.SystemIndicator { + _init() { + super._init(); + + // Create the icon for the indicator + this._indicator = this._addIndicator(); + this._indicator.icon_name = 'org.gnome.Shell.Extensions.GSConnect-symbolic'; + // Hide the indicator by default + this._indicator.visible = false; + + // Create the toggle menu and associate it with the indicator + this.quickSettingsItems.push(new ServiceToggle()); + + // Add the indicator to the panel and the toggle to the menu + QuickSettingsMenu.addExternalIndicator(this); + } + + destroy() { + // Set enabled state to false to kill the service on destroy + this.quickSettingsItems.forEach(item => item.destroy()); + // Destroy the indicator + this._indicator.destroy(); + super.destroy(); + } +}); + +let serviceIndicator = null; + +export default class GSConnectExtension extends Extension { + lockscreenInput = null; + + constructor(metadata) { + super(metadata); + setup(this.path); + + // If installed as a user extension, this checks the permissions + // on certain critical files in the extension directory + // to ensure that they have the executable bit set, + // and makes them executable if not. Some packaging methods + // (particularly GitHub Actions artifacts) automatically remove + // executable bits from all contents, presumably for security. + Utils.ensurePermissions(); + + // 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(); + } + + enable() { + serviceIndicator = new ServiceIndicator(); + Notification.patchGtkNotificationSources(); + + this.lockscreenInput = new Input.LockscreenRemoteAccess(); + this.lockscreenInput.patchInhibitor(); + } + + disable() { + serviceIndicator.destroy(); + serviceIndicator = null; + Notification.unpatchGtkNotificationSources(); + + if (this.lockscreenInput) { + this.lockscreenInput.unpatchInhibitor(); + this.lockscreenInput = null; + } + } +} diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences new file mode 100755 index 0000000..f9dad23 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/gsconnect-preferences @@ -0,0 +1,127 @@ +#!/usr/bin/env gjs + +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +// -*- mode: js; -*- + +'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 [, path] = /@([^:]+):\d+/.exec(new Error().stack.split('\n')[1]); + const info = Gio.File.new_for_path(path) + .query_info('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); + path = info.get_is_symlink() ? info.get_symlink_target() : path; + + return Gio.File.new_for_path(path).get_parent().get_path(); +} + +imports.searchPath.unshift(get_datadir()); +imports.config.PACKAGE_DATADIR = imports.searchPath[0]; + +// Bootstrap +const {setup, setupGettext} = imports.utils.setup; +setup(imports.config.PACKAGE_DATADIR); +setupGettext(); + +// Local Imports +const Config = imports.config; +const Settings = imports.preferences.service; + + +/** + * Class representing the GSConnect service daemon. + */ +const Preferences = GObject.registerClass({ + GTypeName: 'GSConnectPreferences', + Implements: [Gio.ActionGroup], +}, 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)); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ar/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..78e91a6f75e90c3a857590fd6cec7fb914828be9 GIT binary patch literal 20859 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

bs2T#%pQPa8Q4!*~e6g4QsAF%!BGT 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/da/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..3fcc2a5a4de4f97c40e921d8cfa043c1a57847a1 GIT binary patch literal 12438 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 GzM*$G!KN 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/el/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/el/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..4a067cac612298ed905a2e8fcf46791422674b3c GIT binary patch literal 23921 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 N^$WPTUA(N*`#%HI{e1uc literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fa/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fa/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..d792ee91077f5fc0b36d05d1622e2dd7df290d22 GIT binary patch literal 21364 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/fr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..80b6edbb413fd35af09ac353356372fa6a042684 GIT binary patch literal 18747 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/id/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/id/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..15e8fa041f12a20e73c8b7917f9b21e8259eddf7 GIT binary patch literal 11530 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/it/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..499500d30cf12030c150222bda68b0f1e41d809a GIT binary patch literal 17949 zcmbW737i~NoyQ9ahv6oIa{53*2xKOZh`?}8CKDW(NyhX<#0zS=YkG?GRJB#zlT6kl z;;QVb2#c&7DtK@y>WViWs3_}wp{pP$0-m_)a>xp+?)UfJtM2Ls{^UQudaqu+ z3UD{@Lm*QQZUqkqZ};`T1T~+B!Ck<=fn(qq3ToYd@wkM+BFf7_efL&S^Laa{ey<0`pO1RH&Eq#ft@B5q z*8eL|^Z6^Ne)eHg_1yuW=Cu@5fBm4^oeYkE>wWzd;3CS``SSZg?b9bg@$swR@!)sB z)4`n(GL1hB9tCa!C3mmp4^4urL5=rGP~&|W)V}@*JOq3K+!1^hg`L2ILGkNwQ1d?4 zmrns%Qg9Y1K1QJUlY%MuYLKlAeh=OU?!qRn27e7|e-D1P>-RWN>pR`!T5vJt5m5b2 zfEvFFieIk)C9m5+&Hoxue7MDzKMjgM-vq_~dqK_PQBdpI!#G?5s^0@Z@ne}UKNr+` z)`04F6cj%zpuVqy>OTfGpKYM#|2k0LT?DHAKY^-$52*GxfEw>pzI+F$c0U5e*I$8} z=Odun?T%7QUJe8$mxqJ8?+a?42Z7S#=YSgj1n>-SEvWup4{DxQ zfv8w;BPjX#5~%O*1;y`QfcpM-pzOw?puYPXD84N@()E7`sCNCJ=6Mpxl!CKC*^4H43ixL5 zXz**k{!vi;-Funq?^&Ss<#15@eLSds&jQ7-O`!HY0kyu1K=Jb$P~UwJlw5wn*MA4x znex59{2-|LJq&98i;i;r?+L2@0B{9(45;@n0zUyxg4&-0mb>*Y2i5*mP~&X?SxWG7 zQ0sj?xC{7hP~Y9;%bx}{pD%*Df?oy2r|*GJ2mb_W-C~m3?FXv=gF&tL7;qe111nLHLq=;)_WnSdEEd?j&Jql?}FmX1EBQa zci>`hx0Oy#_XqXgQ2a=J z{To3=1bmNAX^_? z0*Zg12i4z$;O^iK$2&gn32J^vg4(ZjpzKWuN}eNdKkx!@FK|2f4Db?A>$?t=p4<*< zUEcy5;60%DF!DTn5I70)FF5f8r#C6Mgz_7}oxrO=jdv}m`CaGV-vaJS`3_Kgx(k#% z{Q`U@xaWyZFOLM(UmvJ>4T89s;5<<4z7UlDUIS{p>p|(k&7i)!1JwHN2F35+f@=Q= z_$=@VP~R;%$?d}dpxPbnaWyFYI0f7vdNvf%}11gPQ+GeErwJ!zkb7 z%TIzDf42e0?|nTk10^S?cw7&P|L23^(*?f%RiMUytAGDCQ1bIGQ1kdOsCnH6YF&4N zTF+ge0q^(a9Wf@F=kB2TJHq2~Q0@Cc&GQ6M^EwTbevN}`!Iy*L$9qA^$E~2o{TgV% zdqMT{S5Wf2%PFp(y}%!A4dUGPBgK2Usr5|n)Ggc2SB9tx`c z5UBka0X5D`LABou9t5^Q@%2*hZ15&u|Cq1e=X5vTa!~6y3DkFMLGk%~P<+lnjlUh# z`mO}k{@osL2F0&Wg5t}!L49{WsPFy=O0S-NhMUiRp!nVg9tuu?;%g2b1AY$F``>`# z``&}@`$Is<^>XkK@N`h~co`^uPJ{aHLhvZ?CQ##l9~3_y0yV##*SPf^2<}XI1*mne z2DRTCK=JW>Q1iY3)VQ-CTM@h&6u-X?J_O$7-+yJu@!=<+_T^zv$n`G zX>g6Nf6&)I1TLfgF>nV^a~X&9`vdfk_K9_Sd;$0!NNf9;uPflEJY3)lec-MB`K#b3 zAn|J&`ZV-9=o;u;==IQMNWXuB{sP?x6~E(oc(^Elh*Gd4lRXdo0kjRe5jqhgv!Oen+o114`e|<^hqpj0A?00u0$v6^28GbOpcg{=z1@NR zyMn*}0KFAD9}+{~2s{bh1#N-!dk!R--d_cNFNPj~-U6Kg{S_*HhKKL_!Z*M-LpS;I zI`C>p@_&x6E5pB2*a#)iRnQk9{f>kVhF+-xzdt$%J_I)WvtS5X1WDf)u@>o@bVlxwhI3D8t`KOyaD{Me|{gh8oCzx5p*H+1?b(-4D=i5KW{PlNPpL;nm-sKDxcV{t~<# zItkhV-W&t|4*EFsIP?hgDD)!eo6sC|FLV%eCG<>aN9bkHQfP0e_$}e#Z_pFa-$6S= z#qT2ja3Was&z~w@g0F@Shi->H3%wuu8gv4*2KqAePUueP_s|ERzlR=#^qX|>lz&h5 zwO;{ugMJJB7j!7J6Qtj3pFYLu{m?RBexAp>!F!-L`LZB| zj(|Q7#ZdWoX{nI$d=|9b*A00575EU8`?BC?&~?xh^b+W$&<#-e_YMkwhVF;Ppu-^h zTUs+iX_7?MR#Xe-7iQw5-HKH0o^l?SE;UCjt=)goCzc+&s%ADuGhv*>NqzZ(SDTVn z+%W6fjYjcqIB&%>VT;!HZ@=DG4o!zy-40F1S!bTQ_wK+2x@r`NcYG+?0 zX^WSQbW2n-t<8;sirf{M%0ST*=vUjE}B?%Gf~oFp=n~K(#)K> ze#6*sFgTgETjtD)XLm4Itwzn3g*>M(zNofxlTMk|bYz-goH4&zG#ghV8rjY&(=4mn zZsoROHe~4aX)B(Jt9Gzpu%_LdnF*5`&$D4tg=2$B%`B~%lwPx9n4U^{qsdH~MQlqj z*lad@e|g|p7OaU3EN39`)UKln``I8q3~Lv_M3c|ut!T!~rj7PYWNPh9Tc$t!TbU;H zRAu{BuqLjtIJ=~3-I)S$mCZPO~BW-V}{^jeHJae|;oH_}9}nv-dm)w)k+ zTte7+K{1~$6)ST|m7?avhXW@)WmrGeoI zKFw%r6JE^5(H6VS^wZBM`Q}#I1f>>*Mi)iP`=T%!=dI#-EK1tN^LnJ9c&Zr(+LC6QVMLruRm@mgi-O@=+_Ha6q5P^be)e|3!-Gi1EZ0dx!qS;pWeN7U*%8z8Ai*nn6RVADR$Z#hRxBO6XW5en$0z>1sg%mgW&`_ z%ohn;i$YkFW@j)RCiTdiGI`q4{FIfGrv<}XG0du(^rpM4Cc{?su$0)zXbK6(ninEI zytNr+v9-btdTB*TE@Rx;?{y)r!nzLZP!!Kb=Kk&PSeoCz{heki!yjN_x=r#3E7y4s zr|jFU!8+@1*2TOXn=|9nDN43ZUpvMJa(#2AaBw}PUN!CdgwB(hh{BnmTa1!w+7{bn zfD^OK zjq(d^HGFoCZ=SP5V#}^D%9$g20v9@N&~<3G#8~w8VVe-4ov(mN@XblhI$Xn4x|J!n zqIza6BE|!mEce`kJJyefC2wU1!PK?4t}tsVYgZWDT9(GO6{f;=PsRE43KMUrtYE>_ z4HYY*=dgJ=lJd%348kh$Psqga3*%X zX~vu#44w~LgXgofU}Ri=D5E|YVK0fN>i7zLXtyZm7@Q?vnh;);mphNm9cBh+dlh4D zMPx7bUQ=1-PXU;J?sR4H zsNtexT&oESZqW>j%&N)`*AP4*)ogc9t>sEj&B)r`79)(_QER_1W4nd zcEs*;fN^W3!MN6|KLav>T3W^#V+u_EgK*i zq#Az6Xt%O3$*1s?_;RXj05Cp8Ct}rLqoJscLW}qjw1JW=&_?Z&{+PM6o%N0rY-}e* zkdWI#;W8CsENriR;)G*2#>FBgqlc$(xt$P&8CKR!jN3nEu+8bp>bJO13E`IhxG0@P z1r=iEs`SVjZ#M*P_pb9cyiax2npWNlC3n)>j;_)i@zx*Y)<&YP(6cM{wVjUI zO{B)zrS1sShNclGo5ghm-F~1A(P7kD75v=7EmVui(m9Dn3UE3jxgK1!QN+pRzTKN9 zyzoQX<7{~j-RX#7^CA*v*;-ha**Mw8zm>=pHYHsnMnDu)IQz0K`s?h8d&H4$_O)g9 zF*n=NF3UaXq|3>^TD+Qww)$SpIr5%5ylUfZYqu!zV$;S^#lWpCHVn1_ebIw$w8HKI zj+nNsZkO7a+{F)_@Mr~LG=ZE~6T(G%I?N;{lhz0J`_Zw1ozt@KWWh%IrmC+NCo$_C zV6c4$^TER3^0dYB4fpSl;kGJ6fI|$Zgj1)w$_(=ATvmmEz&elnw_ll?I!4-T$v7eh zI>&WbVztq0gnRQ8)6oYV&t*UFSt8%rQ+aWY_r(|5LwYbFx8DNDz!b-YU?QqcCnNwG zC`60UAg87gY2tJCX*RaY(Iy+ar8To=PHHlt1By2iJjIDxDy~3mi{n!##z5`7Mo*;e z>NI}7fAQKZtar=^(irN?8IDl=kt?dUvpmiE7t6UQ2tx;GP}5fh_BLExv4NAek)!yQ zn9v6W&7~-O&|`r67mr3$EkeV%R%=8nNLQG5Q>}@)rp~xt{e#}39R}0N;)d-BiAgfqTG7l3k0}m^nMo9A zY)Up0EhYsj&QTac+1cGq_TWmPi|PP`_^(U7%;1jUA4OJ*c1C!9dA~x8hGyUi$}w})n`S=j>ku%oNwEtO!{=- z8!#iSun~tzr+NVAf8NN%;<1sj;qFw9>pym}Hk#e)lMD@51G$onK%AUxlbM7Fr*4{9 z*LPyKofp_XPUC3}J`9)>C*#&)!niDK^sOWPM2<9R+G2j{s*}mZ>CLG}CFZbG&2h_5 zUi_lIAq*?u_Idu4nX=$z-^khlvwGDDC#_yx`m$KB7FMcO6gOa2zN8~4{q+PZ*_Z41 z*@vxhC-}b3CcKROj+2$mbY+^=UE_XxR_z}y8t47p-kRzB(d_h&J8`kH+g1kiJS{!P zdbU|_TWgQ^@5U340^7ZZtgrZ7|F_1TF1D!ktIDvnr}Wg zxuq`wVnxK!Il#IRytLJA8jdxvWrfVK|GB0cZ-enA z5hNULM0O=skP+*4Y;y)KXU|yG-t@WziP|jAiD(>eG>*?9Ge_(B)k^!?NwceEteXy$ zA$I!~8&XNOI6~kOc1jgeXT)Zjsq4RJHZ&b`BJD1oc$tVr+T`VG$YQK*ByGz$#eh zCDr=LMy05iX%XUB!JCP5`OP9Pk0Oa}UhFd7*mIxF&4(*ceKKb;uV;}$NwV)c?dIqU zy;z{EHch{vcDSJ&XGyE$KoN+N=N&a)uO#7oX&rH9%kF9E^E4Rgu@MF%hRLXEH^la* zd>dA)X|pLqQc@%_oiQ*_#9mQmUxkQeY;7g;>+~rLkX0oGq@iSff>8;lt7Lpx*V0>_ zCDHAiI&ASWgr7S-k{oszdMPc3!D*r_dn@WEIaWh^T6hQ=5puMhE?EiHOB=Ik+OlvQ zaD7Gbpbe9IUBJshx2#}1$~dAZ-HhUtX5niB$X0ss$6Ly|=y+74zRHV&TYB9>u zhuz#g$2YR(otUWm!o~(xjAa{2L4-mAUBQuK>Ngb^WVp>NEG!Pq-z`v&NAZ^nP8qwl zq?}-vm)C_jr$;2ZGz%1@{r`v|TtB4t@u0deuqmO1cMD2x*qeoZgX-H01lZN#hJ1#* zfpj+tZUqu24Ez7_MjHTMwS>LyEMs-0=a|KUny|5t!poHcdw9Q z2$jsLBN)`yGgSwJF}XEvv2!U5=?^?qYDUHb{8Gq8vbMqycZwlJ9paU6GmFtCmrMRU zqr+4R*ZD6CPKX)gsiU=Wn1y6_G}hla_?h+HvwXw_!g0kMWwowJ67Ug+gbCB@y7b{%P*LK}@ zBbZZ+IO%3X7j@ioupgbsuft%L+Nm5_Yu#G8hPhhX*lFWU7m;~cZrd36d18x=$7v5D zv-XcW{irCv*a=f@1tt^E#fhza_tE9Vpdx%7E3BuN?VgUY<#t|$|F^aFPcLf{$mQtFD&2VMN`Ypy$V2vS7OP3=uL5jjK z!f~RPu(;&$=G~5Tms#>jIw-0*i^V9rrEU`zWf`nZE%_~-meMTI(Pj#BBTGS;s04>@ znz=2p0ZyR;zDmb9e_+M5`8a4}JcIZu)+VnsW-Cm!Gyf8LWza31t&tMRqo`E;Y>m|5XXM9rf*b93XV=rGC#G{pvzZhY6{A-J&E@JJqz+y+|EY^L3wdKi| zUXW7YjKsNVp+&OiDSVVpReayIz^Q~#Sg8^IFwAmWNfc%?D)2?PU~XUZf?ErbYOHyNxuy%4qGk4J= zZKe0_>z=u*!2iQ2{`2GtI^-l&#K9#OfI>;oSWACro(r~Hea+L`f;DQ(D_RiU%U;$Z zk|v#WfNy5`Q_eaO59nZR!GY3LH8)%%`^DyzYMQG_4(_h zasWuVD)kgNZsFL%QkWw#)m+@a|m2y-*fn-4&7j5sVtQU0V(jP;0J5{R9VjFa&^Qx^B zuZ`BXA~}-C$c8$WSnHM%KXtq5COC?NE^13h%1KD8k`yM|X4u?G z`JQWt;ancprE9T;3Tq;lwqZA&iS|jou91>1BR^g^S%A`jvIFu3d93>{vWkdLS!k`B zi(#Y^ma=*?{BIN_5osJCn>3peyN)nPckgxkMZF`~%P$F^k94{{vlqHvu!6Cy;FT4K rV4}DVi{U@A8=Mu-%y4avi6T8}PPstEes)B~GwR&CH2Rs>X~BO3S)*I* literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ko/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/ko/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..16bb28ebca6643354acf99015bee95a2e92cbf72 GIT binary patch literal 16896 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 Y%ywmO<9}ybnz}UX+s;2eeO?y)e*yvwDgXcg literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/lt/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..13f9d1c6eec2edc37318742092b9deccef5209a9 GIT binary patch literal 18231 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/nl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/nl/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..e01ae54781c0979cc1ef9baf9297b900d76bdc5b GIT binary patch literal 16332 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/pt_BR/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..bc26e36159f6964c53a2f9fe6bb6c0e37159ec67 GIT binary patch literal 16871 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sk/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..f6831f553eb17e2b32c4ad271a1e442c27d2d150 GIT binary patch literal 17143 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 MM@#=7zb^&<7pyz$4*&oF literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..a2a03e40c1671b52827f82a48514c06a4801ce6d GIT binary patch literal 8802 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 DSg=&# literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr@latin/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sr@latin/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..ddb718ce739b7cb035344465488fc23b50d484b0 GIT binary patch literal 10784 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sv/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/sv/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..d17330ce0a3c0a882ff322347a41549b188c543d GIT binary patch literal 16752 zcmbW7378y5b;lcIY>)wCj1Mlm46Ko{W_1{2vp!eHN{e=-wPqy&#yGt*y}Rw1=^nag zRvKdmgAowGHun+kAejI$!C(|(6Rzb<0))$i+t}P^fN&TvCV82DrO zAb8A)K|s}kfk(q9K$;wEfRBRPe0~Deo-5!{@cD2OUJv#CZ$hdJ?nfgZ4Gq+HH$i=O z+Vc{qcFw@Z!Ud>)yb-FO?}aDBk3#9?JMez+PyYFlC{N{%_k0Xgf6s(!-xyT;%TVon z8hik(Le-P`=T}1Yd%x%FU_|;RsP|6fE~R}EuLkleqILE{}ie{&x5Mx zwNUT91*%;)Le+N&s@x(Rhqw8BgAp7-x&&2jJ=8c|2&Km-l?`(&u^p9R&A?Vi(6^*$A<|MNcGhU&*PQ1xC5rN37|y?+3z{&zsN=T@lpe+=rq zPeYaeqR+n*s{9Y3zIT^TA7xy*lc4mu3aXuthblJ-WgnM7*M6 zQ0;sg)V!@ieSaUk2=0ff?-Nk%{5oU`1wVohgDWsPy?-*4zE6Yl4<(iZIu9eo3;y+44spx~!a<&NMZd*HE9&toXPBya`1%I7~D zs@>0n8lP7{wevi?Bb{{^lfecX`K(}_^;J<@X()O%+_>1_*C{TKV^Gf?em zK&lF!1?3kG!1LgT;p5=lK7Z9|PQRO=>YIQX-`!B(twHH&0jfXOL+S4ZsPVfQO3$D4 z`M1F%Nq^U;e*)E>yP)bn`mwIwE(^tZyNxuZ%5569%pEp2UZ18@_lnTBK z*TCOF)%#c)A^&k9JO}QA_lMU()$=+y0uMr!za6T5zlYN2(Ir>z6sY=7hZ^T?Q2pBN z`E*z&Jqy>tTcGswQ%F_8ieXppS|~kk^n4Pu_6XJP87O;t8GImo8bv)* zu%55EqTM;ppdT?f_w8{rx70}vAoehB499(<->58$z+ z9|6_gVJJP0K-IU&r!R#nHwSC517%0wgWraC`{&<$yqj;ofEtItKz;AXvs}F^q3U^v z=V?Cw9LN+3Ho)Uy4N5Q1h7W?*L%sKAsQL09$PycT2Hpq$%=6dq{-p1M$G{_>;Ph|; zRR4yc%CCdR!Ly;pYa3MkWvFq8p!AnO)pr%tc)t*8{=W&T{sU0uZicFF5vtr*eEv_N z^z=ul`j0!?eSZj^K>7@*_cuWG_hO&E463~e)O%NWz8FfcZ-lbPgHYdlH`My_anEl< z_3JLE@w^vK!N8-T`o9{g{`FA(xxjNHRR6a^_3vVMJPe`quoqqep9|%WZij03-B9Cl z^b;LVgZl0$RC~8U&F`I1?Yau89s8m5_9CeAZ}QLI4lgEslYhPf=cV?Z2-S{9L$!M? zd?-8%N{>5W0-x@me+E97^cSG)@Ox0-{SlO1{Q{l{gL7T}Nl^8i4B0{iC8+jKL5;&6 zsP|$h{a*p~{(jFlzzNdtgPMo;W^q;fPK6q$bD`>=f+xYt{PTTK{d*adUB3~ke;@Q* zgc_GmL4Ei0Q0@4J&;J=zy}yOh<9*I|<97^{-cNy#fKP%O;T${zz7wi_Ux6BzUqZcq z7nFUjxWIkyVNmuk1hqb#1@(LnR6i0Z{X8G4AFqXK=Rv6Y-VHU*w?KX8v+&;VHmH7l z6^_CmL%p~9Lf5Xdq29X~%FZGvJ?0RT3tj++;76dw=?74H_&L;f?}mE+sEb_w!{Hjz zkAtf3sZjPX@6!oXzpjKk;me`w{XSH?eg<(Z!SA8+hiSCz@hn(^8{oa*MaV&f;RxP| z=(-zGo8E(b9(g;W>#2xztLsM&w$A5r|5D@t@)@5m{2THSWV;ev8oxi-yTL!9Po4%* zt8L$@@asNb_ND86$Yy)z_T(Ss{&&c&$lDO3TWxFyu|hmyrF);jSl>`8-7SQC!FRn^zVY&>)&in*X0eK8y?_#r1Q4 zBm15~UV+5O(~$Qg*C54p7dM|qu0kH^GoKD+gRe!#kk=tULB5JiBE|IrZeHdS&xU2> zr^r7dRpb_A6Qb+g$Uh-ZQiAIp4%W9^>F@spZ$^HMjQhL-59H=M$YYTYA$6pU=sE>? z9P&gZxPIke=bPs0qkMW5Jl)?HsBrU4_Z~teB`ajy%Al{KpMzv zkn51YN4|mlEAk)6Zlt&#&du4#SA51_!;_H5BWL)$cfpS#5Ao>&NBWx&!sC$@$QO`H z5naDP{u}u+aukvv_8O|1jcJlZl~z;@mL?i;(r!h{c6T|qLqlfOQ1y-%92#1?zG`+x zjWAB)WbX83k9H-kxNgSU^?LDabKZ&@VT;mtTz!Mj+&CX*bN16zoE5bNL%E4_6V|gR ztai*y#5<;)tDJq1q%9uS)4fsEv{KV<*3+}LK8nf~x^O`6Q5O53-B4RMvu*)D~34SGsP8c{V4O|u?$X2MF%HZ)7?!G?M}YNa&Z ztHOjWGepC(bZ;Il*OapX+Z10WGiar>Bgun}VI#^yfLiJvgZlHvXm&MaRU4gcOvosf zmP=Eq}nuK58f8cM4Zq*XBgDou%)V}O2L?RY6wS2fD4FNsT!dlJCpT;B=^XQW*i45tX{=)~lV)!4I4a9@ zMAOXIEUmUH6foSOr-p_$;lV;2?X|;9J;Qt@ueH)9)U+sObdj{WFA@`R-YV`VqoiHj zZ^sIX`*PIMVB|%b*3dm;1v#9- z=Qc||XeOeEe9ykijKQ2!+ttKYD^K92KTaXx0nJFF*4{L$p@=xwR56ojH3~LYR>-I;9&G+T5iWx-e+)Z-COzLU&Cn`Iumy1af+X|j-eKHoEdLU zDp{v#J=i#Q9&AqV!@Q9&v`h%=(rgRn!(=Wp=gnL&l%Ka|=7M1JJ{+^MX1wd}pC+5_ z)WcKaC!<*`9B*Ej_~w1hD2uHZt_Ncw>w;Y;a5_=E5Scr!e*I8>$JK8zvl-4GchW18 zN4T`^bJSv=?hD53+Av}KhU_MRJY14S=lhp{&g?PX(N*YEp*pBSE1P(?qTp)HRySiun@53l-o^h6wE|A7i_^nH_fDTV8K>Y6l|rL!T6LG zkc|9boN-|zG>4OC@#`hEss&HdBABo*=x_QGm30*pZ1Zyi4HZlJMA(#Tn~3JZO2?EZ z%SH17xiW2c3U}Z$7U-yEbuT;JTe!qQo10*xlm(MhJIBkxq+MsHa2|5zzQ1NeTJ3Zb z%+IVyX|0+}XRv+`({-AYGE#Ci-%m3|k6cDGtOk?XJL-?sm%E#cD_PpKv&Z_S?YIDz z8GK^y7C&XE@2ht096vqS&MMsZG*^aqYh!%9s#2=YBJGUU>)K(FXtCFWc2gs#vr_}3Kcmf!xs4 z9YHP3Evf9meQG+eC+KvIc1=t5vJdmqgsq)D$z?K=8N2Ka`>*5nx+hy-$+Cg;MU`JEPEB?}K*9DI%7?`g z%iUfq!|k4Ai8^kT8yKfslFBUjc5%4l>euFG4kv8(W}ICE9k34f1)E*0Keo_7t9j2J zii=~kPwuv7a{ZVMcA)8#hti99Fz(aqCo-kIHWr>V1= zpBlkn(pmv-ho}ZS349_g$Tiu0F{^7+9C9*~Xx-hF)Yv>F1-0yJRVVFy(k^=dBk6o` zu!|*bnK`(Fh(68uvrV(IJVo}>=BbMTvpz^a&mlfbFV4uhS#ptm@+UW z*};hQ6Kgoc#>u%h{>Dmq{;uh<(mB0y-Zo1dAk!*(7%}I}#I2QVV6w1Y8Y5ytSTAYX zWPbkoa|s{m$@!}ibL#nK-RbA9d~#_c{)Kl-UO)XxS@y6rzG=jqx&G`Yo_S{9n?--s zu@W8Yy{0Y9h*@(<*H(t-5?n|r*Ey=h6pa$A+U_`9%8^SP|u9Mz<66)T@YP&Z~gCb}Kpug-m<)2vu99!p(US zFDxEhQhxD3=K5gsCl(LR`M?1YYL(thIF>HvEwwnFTNM-uAWlSgrY5&l<0*S`a&Zjoz{0GSa&)y_PUZ<1Cpvdg!KSL+y zXg$H&n%3Ve)wDA%ps#&*+S)W&u>!v;Gl^Tygjf`{^7?`2kLrAiR^lw)HK zaLR7@ux@NS(9^ylnvsW%7Hdz+JBsS6B`y{e5t=5p+abamv%8{U8n87@l)QKl=T6MX zz8Y+5&&)*iEUvW_k-wZ?WqGTUY0z9>`CuGj6vPYa$_=_9-cls(80s`Q$PM+y101|V z6I(6D^XM>icH@+Ysj)t~Snr-_W`^=u1$9W4}$iD5T1 zNakvriOtD^WheD|)i~qXP2zIHaMrMk84X-qXDgrNY-@{{?>5DJL)T3rR+B$Fqp^GwukWM4yRsTP8iEju2N)95_Q`V1exgYFQhDAl0=lXiP%|*si^cNKO32}dU5D#m>vJ#hzk!~xDIVdxN>Ovf9)a)KqIZI3<80&Q~ zr?j8diBSwV%!kdcmbG?_r7(Cponv-2e9eMf2a1s5c+47B3!B||Fm@rM#C#L-S&Q*D zw%#aV!&uAWg_)H3<91rHtI3)ZvmT?$?HKv{I$Pk--xDM3_*+)i#NQT!uc5-v*(w-% zJ6M|6em)O8NUe4HtSl(k8GJT5j4P2Me1e_Vv=H}KyuPd1WS}aw79(GjV9V3Zl>_X1 zlG~{<$EgnIzkpL@s-hR$O%$nlQ^lteSY15O!ix|m-W}s9ahIABI(g#uG%1Q3Jcjou z-5gt6Zgo*OqhO21g{nvx zCL)e8v=kdoT6W;KDZ*?WX5K%GMio8gMSgdns9bKJk+BL;buZk!t2NVU6-!2cE!)&v zb=d}*V;Y1B7plW%y3uBqef{XrGIRRz$1Z|0XNE)}3XXhlDB{jq9ESA6!_u<3o8aA| zc3Gk1Sm=V#S+F(M7_-W=g{repObA4)Iy-VU5Uq8ZwPeu4Y3Xwhx*LpMsJDbwH5Hxe zKI727c!(cNtyyWMPSG9~XP4|@v$MO97r)rh6AcPSK1H8VtRj|uT67bgS?fGvI1qup zhXAxD9HWP-6@q4(6hE%ok=7*WkG9$fw1&WqvHjVVLlQaG{Vt0@tGS1bK=)NzQuLXw zX)4E0~L{T#vNZ9w%}<=F7?^ZDX)>$E}fk&?`! zD!*a}7{j)UaprQ|ZiuA^C((P5>3>{MpvHH|qYvO2heZsVw=HTho6W+ALs$!9Y5 zE7+E7*|yk88W!_C$54B-pIscP>yAOr5uz^@(}iA=fuCBoqg$S{_^M`;vxbA+2>ILC z?CoyEqT;#TUupHwY_73;VAtwH+clOl{rFAKtU7YuS;$ym2x8+ww7Bqp541JLZ?FXT z)yD49+y)!Z*o%5ABEcRd7+W6n9%nm;PtF_s&Rl0C6#PcbOO7v0AB7Iwcur>>oVZq1 zg}}RelhE!YvFEMvZXUyadn;y_b}W67@#Q+0_REDrYgsUz&fU}MLFnk7 z9dNj%7qAO-Vz=UbOC|%Nz76GeT-;ivU5X-7hBcJ{8fQEvhyD&4n9oyO(XRvg zA0j&I+{1Q&=_L0(t8lqo;o>2tjT9EzZG~Oq^!mI^#JZd$Slq8YpjD%ml2BXIzC3)U z+X`9-x|2v_X4i2B|YiJ*h~Bd zN{n6pNbR+8S?wBwvi`bLvYp~VnR5cx70pt#aIsQ=J%zZxS{U7uZ!>*kZKz$Ycfj?q zL)NjuL$jnfar^T`*LM^}Y}?#c#Ey;)bouZ3clZX|N`>Ba@?|3Wd=0S9V%8V-j=_g9 z4V9d!$3=ViPf87TYN1~2uT0jX?NhJWL~UK$R73Gt;dmUFe_|)k(xwfzH0y211|Q&) zmhHcMNZy_orkP9l6BHnOwNpnG;HT`$#|etf_9hNW_K$UtN;!48yspA&v3;gK+Z#sE z#^`G3QFtnda@c`}OU9 zXZIZ?0f{08A&Q9iibn7t>Uyja)+5fY@jharm>?>s@dAU!ghY+`{;I0q_Z{#L-w)4x zyN<4|dg`gCo~q^EBM!Vd;P)%!MC6Vq1i{2(g5ZKDD>n${pArPe!5H#ia1DQshi`-@ zz>oO+ufan}{}3Jve+{1jpLCRaZx|j)dK4Z3x51V03aC%L89pAq4;}`0L;eds%bzF0 zyWv6bhwvo$fX_edXud)EL`c_z(;)u^XYohvu7_&(1)i5f^2(IGoi@~aS3>pcI;i$u2UTwu+yFo9^M3~q zBz-WGt$K$-&BF;$`WuA$-YR%19EF~kAn|D>E+i@ z{d)|`R{9Vqy`R7z>Fe20dbk8;a5JQf!KdN(;oWc*ypv979v^^e=dl>E#&MYEQE-6t z@lfrpfU0*6lwQ`rli>tZ|C>BJE6XJoloBaRqs7edi?}c zKlee^`!1AS{2a>uehm+Y2V?Yl?*ypy8Bp~v^qhbi&vvMO%|i974)tCeO0TbgdjIuM zfH-9{`;Z&`3tCZ`kzqWKj`V$0z3+;y-T6`S%;WX z(1Dmla1+$~AAr*L9;o*}37-kS=<^?h>gR8u=KZ%&<2uy1_l|{n@99wO4MN$`IZ*w* z7^-~Cr=Jg{hfAUCJ%XwiL-jL-nva)3_3LIR{k;`3#NZC7`d@--_v=vl{4SJUehD>i z4@2c2igVSt4Yc+H)xQZSJG~T2e+{ViuJ-90p!)YtXy*y)y*oTV4)xw=q4e=BsP@0_ z%O8UJ{_mjH|Di1Q3*hNcdYXfJFN4x+2Wq~qhx*Q~P!&`D57MpzPqNQ(gbhfVi6AVkkR$5!85I3boE&1KCP~H$j>X zc0={=+feiR3!nZyJd^Yhr@3}V;X$OgL4E(vq57Xg>G{>3H$zktyaUQ^_CtO5J5c@q z5!Ck{fO`M2r#rnJ=V_qcI|WML=R?)o2BnV_;;MqTKEdrPnV&jpJ^p`T42O z{~xISA9RN6$Kh~*^l?6YGF15>H1J#~ySx-$2{Wkr4?wm5FqB>orc<)V6QI6-8dSaW zpuRr}H9r-oam7&Ixe}_KS3V-LQ0*K)lX8qX9||1X0F!wl+s9k>Xu zhw`^iSjnCTPlD&dLog!E<0yO*yd26768KnHK()66s()9*L*Y$O_1_6uqQM8D^zu!p z`aglH|0^haKJY9TXPgMtkBgzk^Fk>7RH4;Bd?M*9;UVxkpS}gE-n*dm`(F5Xcqdf7 z&v<^-=idXRm;2#i@Ik2e{{Yqgb-YBedoha?Jq&q`x?}^ehd$Q{|lwJ|AVUkM2tc8PK2tr3Tj;Ip!&55o&vW+jq_@# z@7)O1&s(9!^?sk;4fWn#@KpFYU;azDiS%!vzBhJ``~GE6^N~QU+iRh||8CDapxXNg zR6jliWgkC=n!g9(5%9mE#x;OZsXxa+ednoA`Zx#bJC{JccNvsj)S&#t%ivSs)lhnR z15`iW3^fn8!lU3_Q1!n7)vs?smHz;$U%!RY(*e(R<2xMc`zJxQKMaq8qwq+$-KQH+ z?Og*2ErJ`M^zi{Gd;2of`}aWg>mjJ`Jq$Je;5n|{BcbY@2KC*G;4$!dQ2m;L@>@+f z2(N?d;U1{({{iZ~f%BaHj)q#FLr~w_;PamkweIHOvG95*yLdO$_do8_UxsS$UZ{Tm z%%>lM8vpO1+CBWa?)%3<>E~p~(1O)a@4W>60q*qWJ6F5!+yG_A?}8fNM$&qC?( zcTnRx@qAbB6e#;R52~LRLA_V;=@1@EdI8D~ZuI3h`SM%gi^$&tWd}zR%&7mz!c(B} z>Ge?Goq~(-GRSspeed;9zwaS8+9%>}xP++9^7jG%@GZ|7_>!`)k8k1mZ;<1VJCG|7 zeOq>;-&x2u`-DG$7x`xa^#q?n{t?-VNawFc+KBApSIA!@{lA}+kPfdy8i?lWJ&1m{ zAcSPWt;ko9w<8hqJmgyBK}5fK8Mk!*@!_{F4o?F z-Pb1JDM*5BM)Z3t@_J;268zqYY)7s_E<@HK`kjpY8*%{hB;-xVUPQlZkar;q$cqrI ziSqYx9{$BA{@(K#_(S9#B=dP|VT|04oQeD^azAnxQvUvqhjWoDkx}FlM8C&61h>IY zAm2rH`@FZoS0I0jyus&v9o~x!BR@qpB40$_j9iL*3DNI64#8>g+x~eGUXGlKeA(yq z;{>00mFElK!N>zXEo?znA}>QGkY6B&AQj{cM8D&a8uClzbmV+w7Ab$b{KK=#4ER?_ zitI;rBIWN+9zKkmgM0zmf!u`Xcc??~6VHF}e68mgywE>~@Sl-aB7?{tBuDyxA0+V% ztJiBJLM2#>`<8Ni#RcB z+euQEjps$&2n%Z8chzk^vsE?_tjJBAn=olbVXb3kBC45ou3p)KG%F}fvK>*)6q#u^ zlPs*6**J;lRc=~QGixyh6Q(9=wXznOtIe9QD56$J4bRe%N9NI5Fk-4%qnSiSWG2SO zOJ$f?d5tJ7=x>&q*{o&OZQ49J9*oRn?ZT|9c&!B^)oRo%fUQ@Vrx(?Bk=u-gkXAQk zMLZi5W~vAl<)^I4YjI%?b* zF_Lg5iGtB23R`A0tj^mWjPm`u)yaI6G)>XU+SJNWvRGX94V&q(E{$~zZ#8qn zYEoV74nxH-TUo8G88JMer-osoQwkR1XosE+?F{pkyk2BYXy>n-Sf8}IFUvN@c~L%3 zMrpfz-o)&d&y}dqWaMRC=N?9gTLMjR$)Q@b$}wRp~D0TeEK zS&0R$tmWl4Yd*};4|E=o^GU3u_J5MQAC_;KFnlR zi-Pf5T-ZORTsF!vzV~*((ffW-ylDN7%vrzjcIw98VxsLqG!Tx2eo zxoAax!OEG7f^lo-5wjE3qFKzTCYPWknp4({TCw#535g;~bGd`Aa@U49AnTA?OnST! znftDK>x%rotKMd2TUkR(uvaCISjF9Px?)Rr25YSoTN_h4*)db|85X@(Z@W%EzLMr*-j%yDCh6I=CGM!sk7FcKeba&x~3lfrop7iAq@9@FHW7`G!;sc{sI?x+ujdvsF~uP3>Z4V>TD8!>KmSq;opKdej=MXK2BODLL~N`N0O}l8t!| z$DAa6$tuaa37)Ot}=s(sAJtSeJeMxjbT&nXk#=NRy(FLSt+|0$o*(9 zE|;*+SU@>iiM{M>M|THa=DRnti?@QwsVy5S!K8J-Q{Dml@tQSRtwWLZxQ3P7DpT5D zCbJn7Snf6~ zWoYcDZr!rsv|v-VtTb1KcSURYdQE9utlMJTbGIpzM~Mrw(1i(0ZSe~3k5Tn&$gsy_ z)lB!YT&tC3IUB~7*Vw?92Q(;P!F%@b?>^uiJw;~IsG+zB#Ef+YI zwC7@ClPQ^g8UN7@-U_BL^fCyuiQJ6mRqMuW2xExL)`Zo1-VE7c?RJ|-g>_r?(xgl0 zKO7Kg%x0Qv-ER5*b-gA41-37m2F6y?QWvm z8+MNsy@1yG2K&N71e!_5{Wblw?6WXKJ)tF_QH~^uq8kzgaTc@zg`CG0DR@geEjP_P zsH~lyvVY7-yT}@Lsi?t9$cOdEh0>@ns1WN_Ws=qp)@(S!j&>UbGGGt9x8I&zwq_zP zLQSETK-cW4j-}`iCScdIODr3pEwrwa~6)v{}2g$cW1CA)$%N0T_&sCz0mcawi3>!dwN1kE-o))W1qa# zh3V7Ue2QRNVNw|(2Gdb>KIOEOU`quXR8CG3spB2{v=H0jNb$z1xn|aMWMR{aNuBZQ z_!y_Esof22mJxGzJk#Yi5KL$7>O8y6@W5Croa+X+SV*Wh8$`|gkwdJuTY1(R9*~Pu zD8PE7LQQX#TwZaYVz+OD-Z*{jh}nQ=B6gM3&ajK&eUF9*Hb%1r0ZLq}CDEX1#dGt8 znQ0eAmJV9m!L3J$$=Uq~9fpJHR+#3qQOme!wU@56Ip5|vr{`{7OH0UZ7}K4mjx~Pm z1Ri@Vdsd;?4L8`T z!>wJTmlCI!$83YQwOfm~3&QYrtcJ9CCF3f5>ttw8YvDtWzhTY-E z8`=yJD_I(xmL-+#FfQK`S?v^>r6k1L4^;`^W6rTttxS8)(1zL&8jvloHgn}MZ@_IA zLzDSjTpL=`p38@(v(;u`YV(jb3)LPPBk){p&OY<(RYPZGXqLpo8cHq-4^ok+Nwy_#!)%cr*tOm3JQ?{(#@;WGy`hZyOQ%!692 zw3VFi;`DqS#ae7+7jB(iJ9J*Jp0~vzPIOrfJ*+n8&BVn3aY8FhhSqYKz|~ILw8{L! zv(M-Fu96E+P0cedG-sW5{=f@{Msaq$JLL7#uarj>4s94)ZC0KAoae4u)%RvOUJa}- zTZX^dth}siD8q9p4sa;f`DTd0xg+9GcM@LAe8=g^X0|eG&AH0M_AoR&URKVBd#yFI z;t^l7-8<{N0o}u`3HR+v_U(SeV=b)5^-!7JCtcG`gbDUrS=x(9b+7Eo%eb*DX@ssY zj~`t;9nN#Kh*=l@g?gCO^x@nrVlkSFHOtGZVKsa1|jB3HwtmCevQjabFVBM$)z!V@rGY@1iL)LtRP&cXW!E zZYMH~>Uqq6i?*@;^G&-m*G|GlnA6j)O3nJPUf;Jnt%bovIMYrHo5aMv-FevI{%5>H zXA^u!u*UW$Zp3E1sK;p%?cY@p*iK{(?)%0W6X&!f-oLAX1eZ#q^lksHdT8q8#LKdh zutg83R14=rGaD`@>XMwyR^LNv=rH|_}RlS_R zOLBp???cTZO#1rlGU$4+o%vqcTP*EOB2wl)4!ptAd_|SNONXP7P0+#2ERov5IP& zPL!gn)L^U_Nt?^fy4S)S7c2E7Hlg(9T9A@E7%dzB+|u4OS=ze~!`4j-fd~b|!DN_+ zOM7#sEEtsmu*$7r$ckM?-@N%8ox!C)SI!Pd1KsyWv{GHk2y$^|I~a4)(u2=E!FK6zhJDt&p*?D(sqXMCEj9qOFtR^Uhz)v7mZa zhs=v9DD-G*H97p+<;#= z*tc$~do|{DSE&kX!#X#%a=n@{1-G-T-$vW4A!M^UU?no%uyuvmoYdMlmWFZ3uE*>7 z%HEUVQkxE9B0WbsgKbBtI)Y#Je!P?50K=AyDZw3L48vw4L!V`R5Z|`bw%i1s-Yo&| zQsmY$Zi^UK6>Vess6rDVCx?IcE5kW`dF_3>tVinhrPr&$M_>Q3)XLkx8S4tIHZowoT};SgpG}ji;7s5)+!#Nj_2+9oXy@}ak=R37Q-u; z-|f!J2jdIwa*>cd)?UX1E?vvQ#X4nLKIe9q!;K`j;)dngVA=O?&izU$2P986k%S8! zZeRUMvSZ|~f6;rs@f`QS(HP%BzZqIEEu-i;3=X~&XrY&phs*~n{3BJm(S!G-W=*-d z%U{^QqIdnsp0T-q&tMCXg5xVO~_qT6VJDRztxU_7E7qL-gzY_#8yp?*K9y{qBeZ|+UPQWF&M0;7*IDgt z&IsCf0=psFvi0oTTBx~ggpH5zDbe~3CQ&=bAheCDlRyi7~py=f~alrf_ZrM>F=cS^FLr^483KIk69lcgE*K1$%?3 zc_*?^hsHQ)5F-@9TJAAf8sRL5G&0;@dsy?HUc>I&Ak(Z~C5d#%~q zeQc2p@OwK?uT=KQdfXD#YFOyjMY>`aij=Hf8JAzfEt$6c5A} z%Ffv}qS$n^~rVjkx0n+h)(}v4t+m#Ma6BTLA{>zFNkX zOp5XptSIX2yDHtxO({2YbpkR)oq=xdCKM2|vHKmDmr*&Px^_WZV)tCU8~;wMVG%c~ za}|Z(w#e8=zkszO39~sT*hoN@RffvW3zezv@+vc=m-5-(mKxi7FVyZczBw0tD_n4` zmKp3ooFe+hQWo?g5545Sq0eB=X-vRY3! zgUpaUdtfAfgXStvn*{eYuGhVtSYgH^tJmZ(+Bz4>12G5%K?@PHum0){-wsRHHORn& zzWvf`Wx~dHI0w{@x58j;u*v_fpbMZ5rIay|w#)~FtgIG>8WCi35l1~vd;I?a&LZt- zFx5qwEFMQ5Re?BB2ImtxlV%D~(4Ha=!ZTOq_8F_xGH2Orl$qX@_8n?`j&?xYyS=qt zZ{t~O!UVDPltnkvA@WF_w#&fQav@%1lszTsmV@ae^ubowaBF7|QGDK?;qmL8u*s}( z@kUi!%$P$dNoR>hwCEJU;Wn$cQ@7^vB#TjJc))Ju>~j1IG-KCIe4XoM{ofTFmP_Y5 z!x2YYQfN4Z(WVbzAMx@G{)xLle35(NcBHJ`YpT7-SsVw>*oOxO#=E0nS8csp;MDKqX_N;NOeFK@MB{cA1*ao@j$*uL0qD5Z{`fee|Lb1&P&MVo5aK&qoF4S)W%J?xFuUkvwlEh?5% zt*c>Yk8Y6=e>Ip#wamYEG$PJseUBUlvC3eCW!1mmQ!O@YxbE#{Hn{e{!IVLy)?n8& zjoB4yl^Letyj6wW)3H>=Ylfw#y95r}>*Gb%J{R!JG-p@m0P&4QU`|O{ekVYGOk9Ma)%J?#+TwPO;M-?h@RG7>zS^ z8y?{rBJP8!fB8twMNG=#Ho9dh)|oZT1soE%z*Ju^VRh7^ox}WJ1s4r$Zt?%gaC6T; x&6>@aiMb$CPNa;C^dx~i!qNjvH! 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/locale/zh_CN/LC_MESSAGES/org.gnome.Shell.Extensions.GSConnect.mo new file mode 100644 index 0000000000000000000000000000000000000000..284c93039d4f22c1f8d92ec7319b9c122efcdc58 GIT binary patch literal 14997 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 literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/metadata.json b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/metadata.json new file mode 100644 index 0000000..df6761a --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/metadata.json @@ -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, iOS, macOS and Windows.\n\nPlease report issues on Github!", + "name": "GSConnect", + "shell-version": [ + "45" + ], + "url": "https://github.com/GSConnect/gnome-shell-extension-gsconnect/wiki", + "uuid": "gsconnect@andyholmes.github.io", + "version": 56 +} \ No newline at end of file diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/nautilus-gsconnect.py b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/nautilus-gsconnect.py new file mode 100644 index 0000000..c253612 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/nautilus-gsconnect.py @@ -0,0 +1,200 @@ +# SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +# +# SPDX-License-Identifier: GPL-2.0-or-later + +""" +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 + 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(str(e))) + 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"] + if not props: + continue + + 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, *args): + """Return a list of select files to be sent.""" + # 'args' will depend on the Nautilus API version. + # * Nautilus 4.0: + # `[files: List[Nautilus.FileInfo]]` + # * Nautilus 3.0: + # `[window: Gtk.Widget, files: List[Nautilus.FileInfo]]` + files = args[-1] + + # 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,) diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/org.gnome.Shell.Extensions.GSConnect.gresource b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/org.gnome.Shell.Extensions.GSConnect.gresource new file mode 100644 index 0000000000000000000000000000000000000000..04b844ff7bc350876793714811a0d2eed2bbb7de GIT binary patch literal 65132 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 { + if (row.get_name() === 'shortcuts') + row.set_header(new Gtk.Separator({visible: true})); + }); + } + + get menu() { + if (this._menu === undefined) { + this._menu = this.device_menu; + this._menu.prepend_section(null, this.device.menu); + this.insert_action_group('device', this.device.action_group); + } + + return this._menu; + } + + get_incoming_supported(type) { + const incoming = this.settings.get_strv('incoming-capabilities'); + return incoming.includes(`kdeconnect.${type}`); + } + + get_outgoing_supported(type) { + const outgoing = this.settings.get_strv('outgoing-capabilities'); + return outgoing.includes(`kdeconnect.${type}`); + } + + _onKeynavFailed(widget, direction) { + if (direction === Gtk.DirectionType.UP && widget.prev) + widget.prev.child_focus(direction); + + else if (direction === Gtk.DirectionType.DOWN && widget.next) + widget.next.child_focus(direction); + + return true; + } + + _onSwitcherRowSelected(box, row) { + this.stack.set_visible_child_name(row.get_name()); + } + + _onSectionRowActivated(box, row) { + if (row.widget !== undefined) + row.widget.active = !row.widget.active; + } + + _onToggleRowActivated(box, row) { + const widget = row.get_child().get_child_at(1, 0); + widget.active = !widget.active; + } + + _onEncryptionInfo() { + const dialog = new Gtk.MessageDialog({ + buttons: Gtk.ButtonsType.OK, + text: _('Encryption Info'), + secondary_text: this.device.encryption_info, + modal: true, + transient_for: this.get_toplevel(), + }); + dialog.connect('response', (dialog) => dialog.destroy()); + dialog.present(); + } + + _deviceAction(action, parameter) { + this.action_group.activate_action(action.name, parameter); + } + + dispose() { + if (this._commandEditor !== undefined) + this._commandEditor.destroy(); + + // Device signals + this.device.action_group.disconnect(this._actionAddedId); + this.device.action_group.disconnect(this._actionRemovedId); + + // GSettings + for (const settings of Object.values(this._pluginSettings)) + settings.run_dispose(); + + this.settings.disconnect(this._keybindingsId); + this.settings.disconnect(this._disabledPluginsId); + this.settings.disconnect(this._supportedPluginsId); + this.settings.run_dispose(); + } + + pluginSettings(name) { + if (this._pluginSettings === undefined) + this._pluginSettings = {}; + + if (!this._pluginSettings.hasOwnProperty(name)) { + const meta = imports.service.plugins[name].Metadata; + + this._pluginSettings[name] = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(meta.id, -1), + path: `${this.settings.path}plugin/${name}/`, + }); + } + + return this._pluginSettings[name]; + } + + _setupActions() { + this.actions = new Gio.SimpleActionGroup(); + this.insert_action_group('settings', this.actions); + + let settings = this.pluginSettings('battery'); + this.actions.add_action(settings.create_action('send-statistics')); + this.actions.add_action(settings.create_action('low-battery-notification')); + this.actions.add_action(settings.create_action('custom-battery-notification')); + this.actions.add_action(settings.create_action('custom-battery-notification-value')); + this.actions.add_action(settings.create_action('full-battery-notification')); + + settings = this.pluginSettings('clipboard'); + this.actions.add_action(settings.create_action('send-content')); + this.actions.add_action(settings.create_action('receive-content')); + + settings = this.pluginSettings('contacts'); + this.actions.add_action(settings.create_action('contacts-source')); + + settings = this.pluginSettings('mousepad'); + this.actions.add_action(settings.create_action('share-control')); + + settings = this.pluginSettings('mpris'); + this.actions.add_action(settings.create_action('share-players')); + + settings = this.pluginSettings('notification'); + this.actions.add_action(settings.create_action('send-notifications')); + this.actions.add_action(settings.create_action('send-active')); + + settings = this.pluginSettings('sftp'); + this.actions.add_action(settings.create_action('automount')); + + settings = this.pluginSettings('share'); + this.actions.add_action(settings.create_action('receive-files')); + + settings = this.pluginSettings('sms'); + this.actions.add_action(settings.create_action('legacy-sms')); + + settings = this.pluginSettings('systemvolume'); + this.actions.add_action(settings.create_action('share-sinks')); + + settings = this.pluginSettings('telephony'); + this.actions.add_action(settings.create_action('ringing-volume')); + this.actions.add_action(settings.create_action('ringing-pause')); + this.actions.add_action(settings.create_action('talking-volume')); + this.actions.add_action(settings.create_action('talking-pause')); + this.actions.add_action(settings.create_action('talking-microphone')); + + // Pair Actions + const encryption_info = new Gio.SimpleAction({name: 'encryption-info'}); + encryption_info.connect('activate', this._onEncryptionInfo.bind(this)); + this.actions.add_action(encryption_info); + + const status_pair = new Gio.SimpleAction({name: 'pair'}); + status_pair.connect('activate', this._deviceAction.bind(this.device)); + this.settings.bind('paired', status_pair, 'enabled', 16); + this.actions.add_action(status_pair); + + const status_unpair = new Gio.SimpleAction({name: 'unpair'}); + status_unpair.connect('activate', this._deviceAction.bind(this.device)); + this.settings.bind('paired', status_unpair, 'enabled', 0); + this.actions.add_action(status_unpair); + } + + /** + * Sharing Settings + */ + _sharingSettings() { + // Share Plugin + const settings = this.pluginSettings('share'); + + settings.connect( + 'changed::receive-directory', + this._onReceiveDirectoryChanged.bind(this) + ); + this._onReceiveDirectoryChanged(settings, 'receive-directory'); + + // Visibility + this.desktop_list.foreach(row => { + const name = row.get_name(); + row.visible = this.get_outgoing_supported(`${name}.request`); + }); + + // Separators & Sorting + this.desktop_list.set_header_func(rowSeparators); + + this.desktop_list.set_sort_func((row1, row2) => { + row1 = row1.get_child().get_child_at(0, 0); + row2 = row2.get_child().get_child_at(0, 0); + return row1.label.localeCompare(row2.label); + }); + this.share_list.set_header_func(rowSeparators); + + // Scroll with keyboard focus + const sharing_box = this.sharing_page.get_child().get_child(); + sharing_box.set_focus_vadjustment(this.sharing_page.vadjustment); + + // Continue focus chain between lists + this.desktop_list.next = this.share_list; + this.share_list.prev = this.desktop_list; + } + + _onReceiveDirectoryChanged(settings, key) { + let receiveDir = settings.get_string(key); + + if (receiveDir.length === 0) { + receiveDir = GLib.get_user_special_dir( + GLib.UserDirectory.DIRECTORY_DOWNLOAD + ); + + // Account for some corner cases with a fallback + const homeDir = GLib.get_home_dir(); + + if (!receiveDir || receiveDir === homeDir) + receiveDir = GLib.build_filenamev([homeDir, 'Downloads']); + + settings.set_string(key, receiveDir); + } + + if (this.receive_directory.get_filename() !== receiveDir) + this.receive_directory.set_filename(receiveDir); + } + + _onReceiveDirectorySet(button) { + const settings = this.pluginSettings('share'); + const receiveDir = settings.get_string('receive-directory'); + const filename = button.get_filename(); + + if (filename !== receiveDir) + settings.set_string('receive-directory', filename); + } + + /** + * Battery Settings + */ + async _batterySettings() { + try { + this.battery_device_list.set_header_func(rowSeparators); + this.battery_system_list.set_header_func(rowSeparators); + const settings = this.pluginSettings('battery'); + const oldLevel = settings.get_uint('custom-battery-notification-value'); + this.battery_custom_notification_value.set_value(oldLevel); + + // If the device can't handle statistics we're done + if (!this.get_incoming_supported('battery')) { + this.battery_system_label.visible = false; + this.battery_system.visible = false; + return; + } + + // Check UPower for a battery + const hasBattery = await new Promise((resolve, reject) => { + Gio.DBus.system.call( + 'org.freedesktop.UPower', + '/org/freedesktop/UPower/devices/DisplayDevice', + 'org.freedesktop.DBus.Properties', + 'Get', + new GLib.Variant('(ss)', [ + 'org.freedesktop.UPower.Device', + 'IsPresent', + ]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + const variant = connection.call_finish(res); + const value = variant.deepUnpack()[0]; + const isPresent = value.get_boolean(); + + resolve(isPresent); + } catch (e) { + resolve(false); + } + } + ); + }); + + this.battery_system_label.visible = hasBattery; + this.battery_system.visible = hasBattery; + } catch (e) { + this.battery_system_label.visible = false; + this.battery_system.visible = false; + } + } + + _setCustomChargeLevel(spin) { + const settings = this.pluginSettings('battery'); + settings.set_uint('custom-battery-notification-value', spin.get_value_as_int()); + } + + /** + * RunCommand Page + */ + _runcommandSettings() { + // Scroll with keyboard focus + const runcommand_box = this.runcommand_page.get_child().get_child(); + runcommand_box.set_focus_vadjustment(this.runcommand_page.vadjustment); + + // Local Command List + const settings = this.pluginSettings('runcommand'); + this._commands = settings.get_value('command-list').recursiveUnpack(); + + this.command_list.set_sort_func(this._sortCommands); + this.command_list.set_header_func(rowSeparators); + + for (const uuid of Object.keys(this._commands)) + this._insertCommand(uuid); + } + + _sortCommands(row1, row2) { + if (!row1.title || !row2.title) + return 1; + + return row1.title.localeCompare(row2.title); + } + + _insertCommand(uuid) { + const row = new SectionRow({ + title: this._commands[uuid].name, + subtitle: this._commands[uuid].command, + activatable: false, + }); + row.set_name(uuid); + row.subtitle_label.ellipsize = Pango.EllipsizeMode.MIDDLE; + + const editButton = new Gtk.Button({ + image: new Gtk.Image({ + icon_name: 'document-edit-symbolic', + pixel_size: 16, + visible: true, + }), + tooltip_text: _('Edit'), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + editButton.connect('clicked', this._onEditCommand.bind(this)); + editButton.get_accessible().set_name(_('Edit')); + row.get_child().attach(editButton, 2, 0, 1, 2); + + const deleteButton = new Gtk.Button({ + image: new Gtk.Image({ + icon_name: 'edit-delete-symbolic', + pixel_size: 16, + visible: true, + }), + tooltip_text: _('Remove'), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }); + deleteButton.connect('clicked', this._onDeleteCommand.bind(this)); + deleteButton.get_accessible().set_name(_('Remove')); + row.get_child().attach(deleteButton, 3, 0, 1, 2); + + this.command_list.add(row); + } + + _onEditCommand(widget) { + if (this._commandEditor === undefined) { + this._commandEditor = new CommandEditor({ + modal: true, + transient_for: this.get_toplevel(), + use_header_bar: true, + }); + + this._commandEditor.connect( + 'response', + this._onSaveCommand.bind(this) + ); + + this._commandEditor.resize(1, 1); + } + + if (widget instanceof Gtk.Button) { + const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); + const uuid = row.get_name(); + + this._commandEditor.uuid = uuid; + this._commandEditor.command_name = this._commands[uuid].name; + this._commandEditor.command_line = this._commands[uuid].command; + } else { + this._commandEditor.uuid = GLib.uuid_string_random(); + this._commandEditor.command_name = ''; + this._commandEditor.command_line = ''; + } + + this._commandEditor.present(); + } + + _storeCommands() { + const variant = {}; + + for (const [uuid, command] of Object.entries(this._commands)) + variant[uuid] = new GLib.Variant('a{ss}', command); + + this.pluginSettings('runcommand').set_value( + 'command-list', + new GLib.Variant('a{sv}', variant) + ); + } + + _onDeleteCommand(button) { + const row = button.get_ancestor(Gtk.ListBoxRow.$gtype); + delete this._commands[row.get_name()]; + row.destroy(); + + this._storeCommands(); + } + + _onSaveCommand(dialog, response_id) { + if (response_id === Gtk.ResponseType.ACCEPT) { + this._commands[dialog.uuid] = { + name: dialog.command_name, + command: dialog.command_line, + }; + + this._storeCommands(); + + // + let row = null; + + for (const child of this.command_list.get_children()) { + if (child.get_name() === dialog.uuid) { + row = child; + break; + } + } + + if (row === null) { + this._insertCommand(dialog.uuid); + } else { + row.set_name(dialog.uuid); + row.title = dialog.command_name; + row.subtitle = dialog.command_line; + } + } + + dialog.hide(); + } + + /** + * Notification Settings + */ + _notificationSettings() { + const settings = this.pluginSettings('notification'); + + settings.bind( + 'send-notifications', + this.notification_apps, + 'sensitive', + Gio.SettingsBindFlags.DEFAULT + ); + + // Separators & Sorting + this.notification_list.set_header_func(rowSeparators); + + // Scroll with keyboard focus + const notification_box = this.notification_page.get_child().get_child(); + notification_box.set_focus_vadjustment(this.notification_page.vadjustment); + + // Continue focus chain between lists + this.notification_list.next = this.notification_apps; + this.notification_apps.prev = this.notification_list; + + this.notification_apps.set_sort_func(titleSortFunc); + this.notification_apps.set_header_func(rowSeparators); + + this._populateApplications(settings); + } + + _toggleNotification(widget) { + try { + const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); + const settings = this.pluginSettings('notification'); + let applications = {}; + + try { + applications = JSON.parse(settings.get_string('applications')); + } catch (e) { + applications = {}; + } + + applications[row.title].enabled = !applications[row.title].enabled; + row.widget.state = applications[row.title].enabled; + settings.set_string('applications', JSON.stringify(applications)); + + } catch (e) { + logError(e); + } + } + + + + _populateApplications(settings) { + const applications = this._queryApplications(settings); + + for (const name in applications) { + const row = new SectionRow({ + gicon: Gio.Icon.new_for_string(applications[name].iconName), + title: name, + height_request: 48, + widget: new Gtk.Switch({ + state: applications[name].enabled, + margin_start: 12, + margin_end: 12, + halign: Gtk.Align.END, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + }); + + row.widget.connect('notify::active', this._toggleNotification.bind(this)); + this.notification_apps.add(row); + } + } + + _queryApplications(settings) { + let applications = {}; + + try { + applications = JSON.parse(settings.get_string('applications')); + } catch (e) { + applications = {}; + } + + // Scan applications that statically declare to show notifications + const ignoreId = 'org.gnome.Shell.Extensions.GSConnect.desktop'; + + for (const appInfo of Gio.AppInfo.get_all()) { + if (appInfo.get_id() === ignoreId) + continue; + + if (!appInfo.get_boolean('X-GNOME-UsesNotifications')) + continue; + + const appName = appInfo.get_name(); + + if (appName === null || applications.hasOwnProperty(appName)) + continue; + + let icon = appInfo.get_icon(); + icon = (icon) ? icon.to_string() : 'application-x-executable'; + + applications[appName] = { + iconName: icon, + enabled: true, + }; + } + + settings.set_string('applications', JSON.stringify(applications)); + + return applications; + } + + /** + * Telephony Settings + */ + _telephonySettings() { + // Continue focus chain between lists + this.ringing_list.next = this.talking_list; + this.talking_list.prev = this.ringing_list; + + this.ringing_list.set_header_func(rowSeparators); + this.talking_list.set_header_func(rowSeparators); + } + + /** + * Keyboard Shortcuts + */ + _keybindingSettings() { + // Scroll with keyboard focus + const shortcuts_box = this.shortcuts_page.get_child().get_child(); + shortcuts_box.set_focus_vadjustment(this.shortcuts_page.vadjustment); + + // Filter & Sort + this.shortcuts_actions_list.set_filter_func(this._filterPluginKeybindings.bind(this)); + this.shortcuts_actions_list.set_header_func(rowSeparators); + this.shortcuts_actions_list.set_sort_func(titleSortFunc); + + // Init + for (const name in DEVICE_SHORTCUTS) + this._addPluginKeybinding(name); + + this._setPluginKeybindings(); + + // Watch for GAction and Keybinding changes + this._actionAddedId = this.device.action_group.connect( + 'action-added', + () => this.shortcuts_actions_list.invalidate_filter() + ); + this._actionRemovedId = this.device.action_group.connect( + 'action-removed', + () => this.shortcuts_actions_list.invalidate_filter() + ); + this._keybindingsId = this.settings.connect( + 'changed::keybindings', + this._setPluginKeybindings.bind(this) + ); + } + + _addPluginKeybinding(name) { + const [icon_name, label] = DEVICE_SHORTCUTS[name]; + + const widget = new Gtk.Label({ + label: _('Disabled'), + visible: true, + }); + widget.get_style_context().add_class('dim-label'); + + const row = new SectionRow({ + height_request: 48, + icon_name: icon_name, + title: label, + widget: widget, + }); + row.icon_image.pixel_size = 16; + row.action = name; + this.shortcuts_actions_list.add(row); + } + + _filterPluginKeybindings(row) { + return this.device.action_group.has_action(row.action); + } + + _setPluginKeybindings() { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + + this.shortcuts_actions_list.foreach(row => { + if (keybindings[row.action]) { + const accel = Gtk.accelerator_parse(keybindings[row.action]); + row.widget.label = Gtk.accelerator_get_label(...accel); + } else { + row.widget.label = _('Disabled'); + } + }); + } + + _onResetActionShortcuts(button) { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + + for (const action in keybindings) { + // Don't reset remote command shortcuts + if (!action.includes('::')) + delete keybindings[action]; + } + + this.settings.set_value( + 'keybindings', + new GLib.Variant('a{ss}', keybindings) + ); + } + + async _onShortcutRowActivated(box, row) { + try { + const keybindings = this.settings.get_value('keybindings').deepUnpack(); + let accel = keybindings[row.action] || null; + + accel = await Keybindings.getAccelerator(row.title, accel); + + if (accel) + keybindings[row.action] = accel; + else + delete keybindings[row.action]; + + this.settings.set_value( + 'keybindings', + new GLib.Variant('a{ss}', keybindings) + ); + } catch (e) { + logError(e); + } + } + + /** + * Advanced Page + */ + _advancedSettings() { + // Scroll with keyboard focus + const advanced_box = this.advanced_page.get_child().get_child(); + advanced_box.set_focus_vadjustment(this.advanced_page.vadjustment); + + // Sort & Separate + this.plugin_list.set_header_func(rowSeparators); + this.plugin_list.set_sort_func(titleSortFunc); + this.experimental_list.set_header_func(rowSeparators); + + // Continue focus chain between lists + this.plugin_list.next = this.experimental_list; + this.experimental_list.prev = this.plugin_list; + + this._disabledPluginsId = this.settings.connect( + 'changed::disabled-plugins', + this._onPluginsChanged.bind(this) + ); + this._supportedPluginsId = this.settings.connect( + 'changed::supported-plugins', + this._onPluginsChanged.bind(this) + ); + this._onPluginsChanged(this.settings, null); + + for (const name of DEVICE_PLUGINS) + this._addPlugin(name); + } + + _onPluginsChanged(settings, key) { + if (key === 'disabled-plugins' || this._disabledPlugins === undefined) + this._disabledPlugins = settings.get_strv('disabled-plugins'); + + if (key === 'supported-plugins' || this._supportedPlugins === undefined) + this._supportedPlugins = settings.get_strv('supported-plugins'); + + this._enabledPlugins = this._supportedPlugins.filter(name => { + return !this._disabledPlugins.includes(name); + }); + + if (key !== null) + this._updatePlugins(); + } + + _addPlugin(name) { + const plugin = imports.service.plugins[name]; + + const row = new SectionRow({ + height_request: 48, + title: plugin.Metadata.label, + subtitle: plugin.Metadata.description || '', + visible: this._supportedPlugins.includes(name), + widget: new Gtk.Switch({ + active: this._enabledPlugins.includes(name), + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + }); + row.widget.connect('notify::active', this._togglePlugin.bind(this)); + row.set_name(name); + + if (this.hasOwnProperty(name)) + this[name].visible = row.widget.active; + + this.plugin_list.add(row); + } + + _updatePlugins(settings, key) { + for (const row of this.plugin_list.get_children()) { + const name = row.get_name(); + + row.visible = this._supportedPlugins.includes(name); + row.widget.active = this._enabledPlugins.includes(name); + + if (this.hasOwnProperty(name)) + this[name].visible = row.widget.active; + } + } + + _togglePlugin(widget) { + try { + const name = widget.get_ancestor(Gtk.ListBoxRow.$gtype).get_name(); + const index = this._disabledPlugins.indexOf(name); + + // Either add or remove the plugin from the disabled list + if (index > -1) + this._disabledPlugins.splice(index, 1); + else + this._disabledPlugins.push(name); + + this.settings.set_strv('disabled-plugins', this._disabledPlugins); + } catch (e) { + logError(e); + } + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js new file mode 100644 index 0000000..b93c936 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/keybindings.js @@ -0,0 +1,316 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'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 %s').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; + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/service.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/service.js new file mode 100644 index 0000000..d9bc97f --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/preferences/service.js @@ -0,0 +1,648 @@ +// 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 ', + 'Bertrand Lacoste ', + 'Frank Dana ', + ], + 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 ' + 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…'); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/prefs.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/prefs.js new file mode 100644 index 0000000..dd20fd2 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/prefs.js @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Adw from 'gi://Adw'; + +// Bootstrap +import * as Utils from './shell/utils.js'; +import setup from './utils/setup.mjs'; +import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +export default class GSConnectExtensionPreferences extends ExtensionPreferences { + constructor(metadata) { + super(metadata); + setup(this.path); + Utils.installService(); + } + + fillPreferencesWindow(window) { + const widget = new Adw.PreferencesPage(); + window.add(widget); + + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + window.close(); + }); + + Gio.Subprocess.new([`${this.path}/gsconnect-preferences`], 0); + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/gschemas.compiled new file mode 100644 index 0000000000000000000000000000000000000000..60e8035b64140a152d4915064f206ccefcf0f2dd GIT binary patch literal 5316 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 CmX~h; literal 0 HcmV?d00001 diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/org.gnome.Shell.Extensions.GSConnect.gschema.xml b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/org.gnome.Shell.Extensions.GSConnect.gschema.xml new file mode 100644 index 0000000..4dfaddb --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/schemas/org.gnome.Shell.Extensions.GSConnect.gschema.xml @@ -0,0 +1,192 @@ + + + + + + + + + true + + + false + + + true + + + true + + + + + "" + + + "" + + + [] + + + false + + + true + + + + + + + (0, 0) + + + false + + + + + + + "" + + + {} + + + ["sms", "ring", "mount", "commands", "share", "keyboard"] + + + "" + + + false + + + "smartphone" + + + [] + + + [] + + + [] + + + [] + + + "" + + + + + + false + + + true + + + false + Enables custom battery notification + + + + 80 + + + false + + + + + false + + + false + + + + + true + + + + + + true + + + + + true + + + + + true + + + true + + + '{}' + + + + + + + , 'restart': <{'name': 'Restart', 'command': 'systemctl reboot'}>, 'logout': <{'name': 'Log Out', 'command': 'gnome-session-quit --logout --no-prompt'}>, 'poweroff': <{'name': 'Power Off', 'command': 'systemctl poweroff'}>, 'suspend': <{'name': 'Suspend', 'command': 'systemctl suspend'}>}]]> + + + + + true + + + + + true + + + "" + + + + + false + + + + + true + + + + + "lower" + + + false + + + "mute" + + + true + + + true + + + + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js new file mode 100644 index 0000000..a5357bf --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/backends/lan.js @@ -0,0 +1,881 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Core = imports.service.core; + + +/** + * TCP Port Constants + */ +const PROTOCOL_PORT_DEFAULT = 1716; +const PROTOCOL_PORT_MIN = 1716; +const PROTOCOL_PORT_MAX = 1764; +const TRANSFER_MIN = 1739; +const TRANSFER_MAX = 1764; + + +/* + * One-time check for Linux/FreeBSD socket options + */ +var _LINUX_SOCKETS = true; + +try { + // This should throw on FreeBSD + Gio.Socket.new( + Gio.SocketFamily.IPV4, + Gio.SocketType.STREAM, + Gio.SocketProtocol.TCP + ).get_option(6, 5); +} catch (e) { + _LINUX_SOCKETS = false; +} + + +/** + * Configure a socket connection for the KDE Connect protocol. + * + * @param {Gio.SocketConnection} connection - The connection to configure + */ +function _configureSocket(connection) { + try { + if (_LINUX_SOCKETS) { + connection.socket.set_option(6, 4, 10); // TCP_KEEPIDLE + connection.socket.set_option(6, 5, 5); // TCP_KEEPINTVL + connection.socket.set_option(6, 6, 3); // TCP_KEEPCNT + + // FreeBSD constants + // https://github.com/freebsd/freebsd/blob/master/sys/netinet/tcp.h#L159 + } else { + connection.socket.set_option(6, 256, 10); // TCP_KEEPIDLE + connection.socket.set_option(6, 512, 5); // TCP_KEEPINTVL + connection.socket.set_option(6, 1024, 3); // TCP_KEEPCNT + } + + // Do this last because an error setting the keepalive options would + // result in a socket that never times out + connection.socket.set_keepalive(true); + } catch (e) { + debug(e, 'Configuring Socket'); + } +} + + +/** + * Lan.ChannelService consists of two parts: + * + * The TCP Listener listens on a port and constructs a Channel object from the + * incoming Gio.TcpConnection. + * + * The UDP Listener listens on a port for incoming JSON identity packets which + * include the TCP port, while the IP address is taken from the UDP packet + * itself. We respond by opening a TCP connection to that address. + */ +var ChannelService = GObject.registerClass({ + GTypeName: 'GSConnectLanChannelService', + Properties: { + 'certificate': GObject.ParamSpec.object( + 'certificate', + 'Certificate', + 'The TLS certificate', + GObject.ParamFlags.READWRITE, + Gio.TlsCertificate.$gtype + ), + 'port': GObject.ParamSpec.uint( + 'port', + 'Port', + 'The port used by the service', + GObject.ParamFlags.READWRITE, + 0, GLib.MAXUINT16, + PROTOCOL_PORT_DEFAULT + ), + }, +}, class LanChannelService extends Core.ChannelService { + + _init(params = {}) { + super._init(params); + + // Track hosts we identify to directly, allowing them to ignore the + // discoverable state of the service. + this._allowed = new Set(); + + // + this._tcp = null; + this._tcpPort = PROTOCOL_PORT_DEFAULT; + this._udp4 = null; + this._udp6 = null; + + // Monitor network status + this._networkMonitor = Gio.NetworkMonitor.get_default(); + this._networkAvailable = false; + this._networkChangedId = 0; + } + + get certificate() { + if (this._certificate === undefined) + this._certificate = null; + + return this._certificate; + } + + set certificate(certificate) { + if (this.certificate === certificate) + return; + + this._certificate = certificate; + this.notify('certificate'); + } + + get channels() { + if (this._channels === undefined) + this._channels = new Map(); + + return this._channels; + } + + get port() { + if (this._port === undefined) + this._port = PROTOCOL_PORT_DEFAULT; + + return this._port; + } + + set port(port) { + if (this.port === port) + return; + + this._port = port; + this.notify('port'); + } + + _onNetworkChanged(monitor, network_available) { + if (this._networkAvailable === network_available) + return; + + this._networkAvailable = network_available; + this.broadcast(); + } + + _initCertificate() { + if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) { + const error = new Error(); + error.name = _('OpenSSL not found'); + error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`; + throw error; + } + + const certPath = GLib.build_filenamev([ + Config.CONFIGDIR, + 'certificate.pem', + ]); + const keyPath = GLib.build_filenamev([ + Config.CONFIGDIR, + 'private.pem', + ]); + + // Ensure a certificate exists with our id as the common name + this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath, + this.id); + + // If the service ID doesn't match the common name, this is probably a + // certificate from an older version and we should amend ours to match + if (this.id !== this._certificate.common_name) + this._id = this._certificate.common_name; + } + + _initTcpListener() { + try { + this._tcp = new Gio.SocketService(); + + let tcpPort = this.port; + const tcpPortMax = tcpPort + + (PROTOCOL_PORT_MAX - PROTOCOL_PORT_MIN); + + while (tcpPort <= tcpPortMax) { + try { + this._tcp.add_inet_port(tcpPort, null); + break; + } catch (e) { + if (tcpPort < tcpPortMax) { + tcpPort++; + continue; + } + + throw e; + } + } + + this._tcpPort = tcpPort; + this._tcp.connect('incoming', this._onIncomingChannel.bind(this)); + } catch (e) { + this._tcp.stop(); + this._tcp.close(); + this._tcp = null; + + throw e; + } + } + + async _onIncomingChannel(listener, connection) { + try { + const host = connection.get_remote_address().address.to_string(); + + // Create a channel + const channel = new Channel({ + backend: this, + certificate: this.certificate, + host: host, + port: this.port, + }); + + // Accept the connection + await channel.accept(connection); + channel.identity.body.tcpHost = channel.host; + channel.identity.body.tcpPort = this._tcpPort; + channel.allowed = this._allowed.has(host); + + this.channel(channel); + } catch (e) { + debug(e); + } + } + + _initUdpListener() { + // Default broadcast address + this._udp_address = Gio.InetSocketAddress.new_from_string( + '255.255.255.255', this.port); + + try { + this._udp6 = Gio.Socket.new(Gio.SocketFamily.IPV6, + Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP); + this._udp6.set_broadcast(true); + + // Bind the socket + const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV6); + const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port); + this._udp6.bind(sockAddr, true); + + // Input stream + this._udp6_stream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: this._udp6.fd, + close_fd: false, + }), + }); + + // Watch socket for incoming packets + this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null); + this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6)); + this._udp6_source.attach(null); + } catch (e) { + this._udp6 = null; + } + + // Our IPv6 socket also supports IPv4; we're all done + if (this._udp6 && this._udp6.speaks_ipv4()) { + this._udp4 = null; + return; + } + + try { + this._udp4 = Gio.Socket.new(Gio.SocketFamily.IPV4, + Gio.SocketType.DATAGRAM, Gio.SocketProtocol.UDP); + this._udp4.set_broadcast(true); + + // Bind the socket + const inetAddr = Gio.InetAddress.new_any(Gio.SocketFamily.IPV4); + const sockAddr = Gio.InetSocketAddress.new(inetAddr, this.port); + this._udp4.bind(sockAddr, true); + + // Input stream + this._udp4_stream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: this._udp4.fd, + close_fd: false, + }), + }); + + // Watch input socket for incoming packets + this._udp4_source = this._udp4.create_source(GLib.IOCondition.IN, null); + this._udp4_source.set_callback(this._onIncomingIdentity.bind(this, this._udp4)); + this._udp4_source.attach(null); + } catch (e) { + this._udp4 = null; + + // We failed to get either an IPv4 or IPv6 socket to bind + if (this._udp6 === null) + throw e; + } + } + + _onIncomingIdentity(socket) { + let host; + + // Try to peek the remote address + try { + host = socket.receive_message([], Gio.SocketMsgFlags.PEEK, null)[1] + .address.to_string(); + } catch (e) { + logError(e); + } + + // Whether or not we peeked the address, we need to read the packet + try { + let data; + + if (socket === this._udp6) + data = this._udp6_stream.read_line_utf8(null)[0]; + else + data = this._udp4_stream.read_line_utf8(null)[0]; + + // Discard the packet if we failed to peek the address + if (host === undefined) + return GLib.SOURCE_CONTINUE; + + const packet = new Core.Packet(data); + packet.body.tcpHost = host; + this._onIdentity(packet); + } catch (e) { + logError(e); + } + + return GLib.SOURCE_CONTINUE; + } + + async _onIdentity(packet) { + try { + // Bail if the deviceId is missing + if (!packet.body.hasOwnProperty('deviceId')) + return; + + // Silently ignore our own broadcasts + if (packet.body.deviceId === this.identity.body.deviceId) + return; + + debug(packet); + + // Create a new channel + const channel = new Channel({ + backend: this, + certificate: this.certificate, + host: packet.body.tcpHost, + port: packet.body.tcpPort, + identity: packet, + }); + + // Check if channel is already open with this address + if (this.channels.has(channel.address)) + return; + + this._channels.set(channel.address, channel); + + // Open a TCP connection + const address = Gio.InetSocketAddress.new_from_string( + packet.body.tcpHost, packet.body.tcpPort); + + const client = new Gio.SocketClient({enable_proxy: false}); + const connection = await client.connect_async(address, + this.cancellable); + + // Connect the channel and attach it to the device on success + await channel.open(connection); + + this.channel(channel); + } catch (e) { + logError(e); + } + } + + /** + * Broadcast an identity packet + * + * If @address is not %null it may specify an IPv4 or IPv6 address to send + * the identity packet directly to, otherwise it will be broadcast to the + * default address, 255.255.255.255. + * + * @param {string} [address] - An optional target IPv4 or IPv6 address + */ + broadcast(address = null) { + try { + if (!this._networkAvailable) + return; + + // Try to parse strings as : + if (typeof address === 'string') { + const [host, portstr] = address.split(':'); + const port = parseInt(portstr) || this.port; + address = Gio.InetSocketAddress.new_from_string(host, port); + } + + // If we succeed, remember this host + if (address instanceof Gio.InetSocketAddress) { + this._allowed.add(address.address.to_string()); + + // Broadcast to the network if no address is specified + } else { + debug('Broadcasting to LAN'); + address = this._udp_address; + } + + // Broadcast on each open socket + if (this._udp6 !== null) + this._udp6.send_to(address, this.identity.serialize(), null); + + if (this._udp4 !== null) + this._udp4.send_to(address, this.identity.serialize(), null); + } catch (e) { + debug(e, address); + } + } + + buildIdentity() { + // Chain-up, then add the TCP port + super.buildIdentity(); + this.identity.body.tcpPort = this._tcpPort; + } + + start() { + if (this.active) + return; + + // Ensure a certificate exists + if (this.certificate === null) + this._initCertificate(); + + // Start TCP/UDP listeners + try { + if (this._tcp === null) + this._initTcpListener(); + + if (this._udp4 === null && this._udp6 === null) + this._initUdpListener(); + } catch (e) { + // Known case of another application using the protocol defined port + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ADDRESS_IN_USE)) { + e.name = _('Port already in use'); + e.url = `${Config.PACKAGE_URL}/wiki/Error#port-already-in-use`; + } + + throw e; + } + + // Monitor network changes + if (this._networkChangedId === 0) { + this._networkAvailable = this._networkMonitor.network_available; + this._networkChangedId = this._networkMonitor.connect( + 'network-changed', this._onNetworkChanged.bind(this)); + } + + this._active = true; + this.notify('active'); + } + + stop() { + if (this._networkChangedId) { + this._networkMonitor.disconnect(this._networkChangedId); + this._networkChangedId = 0; + this._networkAvailable = false; + } + + if (this._tcp !== null) { + this._tcp.stop(); + this._tcp.close(); + this._tcp = null; + } + + if (this._udp6 !== null) { + this._udp6_source.destroy(); + this._udp6_stream.close(null); + this._udp6.close(); + this._udp6 = null; + } + + if (this._udp4 !== null) { + this._udp4_source.destroy(); + this._udp4_stream.close(null); + this._udp4.close(); + this._udp4 = null; + } + + for (const channel of this.channels.values()) + channel.close(); + + this._active = false; + this.notify('active'); + } + + destroy() { + try { + this.stop(); + } catch (e) { + debug(e); + } + } +}); + + +/** + * Lan Channel + * + * This class essentially just extends Core.Channel to set TCP socket options + * and negotiate TLS encrypted connections. + */ +var Channel = GObject.registerClass({ + GTypeName: 'GSConnectLanChannel', +}, class LanChannel extends Core.Channel { + + _init(params) { + super._init(); + Object.assign(this, params); + } + + get address() { + return `lan://${this.host}:${this.port}`; + } + + get certificate() { + if (this._certificate === undefined) + this._certificate = null; + + return this._certificate; + } + + set certificate(certificate) { + this._certificate = certificate; + } + + get peer_certificate() { + if (this._connection instanceof Gio.TlsConnection) + return this._connection.get_peer_certificate(); + + return null; + } + + get host() { + if (this._host === undefined) + this._host = null; + + return this._host; + } + + set host(host) { + this._host = host; + } + + get port() { + if (this._port === undefined) { + if (this.identity && this.identity.body.tcpPort) + this._port = this.identity.body.tcpPort; + else + return PROTOCOL_PORT_DEFAULT; + } + + return this._port; + } + + set port(port) { + this._port = port; + } + + /** + * Authenticate a TLS connection. + * + * @param {Gio.TlsConnection} connection - A TLS connection + * @return {Promise} A promise for the operation + */ + async _authenticate(connection) { + // Standard TLS Handshake + connection.validation_flags = Gio.TlsCertificateFlags.EXPIRED; + connection.authentication_mode = Gio.TlsAuthenticationMode.REQUIRED; + + await connection.handshake_async(GLib.PRIORITY_DEFAULT, + this.cancellable); + + // Get a settings object for the device + let settings; + + if (this.device) { + settings = this.device.settings; + } else { + const id = this.identity.body.deviceId; + settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${id}/`, + }); + } + + // If we have a certificate for this deviceId, we can verify it + const cert_pem = settings.get_string('certificate-pem'); + + if (cert_pem !== '') { + let certificate = null; + let verified = false; + + try { + certificate = Gio.TlsCertificate.new_from_pem(cert_pem, -1); + verified = certificate.is_same(connection.peer_certificate); + } catch (e) { + logError(e); + } + + /* The certificate is incorrect for one of two reasons, but both + * result in us resetting the certificate and unpairing the device. + * + * If the certificate failed to load, it is probably corrupted or + * otherwise invalid. In this case, if we try to continue we will + * certainly crash the Android app. + * + * If the certificate did not match what we expected the obvious + * thing to do is to notify the user, however experience tells us + * this is a result of the user doing something masochistic like + * nuking the Android app data or copying settings between machines. + */ + if (verified === false) { + if (this.device) { + this.device.unpair(); + } else { + settings.reset('paired'); + settings.reset('certificate-pem'); + } + + const name = this.identity.body.deviceName; + throw new Error(`${name}: Authentication Failure`); + } + } + + return connection; + } + + /** + * Wrap the connection in Gio.TlsClientConnection and initiate handshake + * + * @param {Gio.TcpConnection} connection - The unauthenticated connection + * @return {Gio.TlsClientConnection} The authenticated connection + */ + _encryptClient(connection) { + _configureSocket(connection); + + connection = Gio.TlsClientConnection.new(connection, + connection.socket.remote_address); + connection.set_certificate(this.certificate); + + return this._authenticate(connection); + } + + /** + * Wrap the connection in Gio.TlsServerConnection and initiate handshake + * + * @param {Gio.TcpConnection} connection - The unauthenticated connection + * @return {Gio.TlsServerConnection} The authenticated connection + */ + _encryptServer(connection) { + _configureSocket(connection); + + connection = Gio.TlsServerConnection.new(connection, this.certificate); + + // We're the server so we trust-on-first-use and verify after + const _id = connection.connect('accept-certificate', (connection) => { + connection.disconnect(_id); + return true; + }); + + return this._authenticate(connection); + } + + /** + * Negotiate an incoming connection + * + * @param {Gio.TcpConnection} connection - The incoming connection + */ + async accept(connection) { + debug(`${this.address} (${this.uuid})`); + + try { + this._connection = connection; + this.backend.channels.set(this.address, this); + + // In principle this disposable wrapper could buffer more than the + // identity packet, but in practice the remote device shouldn't send + // any more data until the TLS connection is negotiated. + const stream = new Gio.DataInputStream({ + base_stream: connection.input_stream, + close_base_stream: false, + }); + + const data = await stream.read_line_async(GLib.PRIORITY_DEFAULT, + this.cancellable); + stream.close_async(GLib.PRIORITY_DEFAULT, null, null); + + this.identity = new Core.Packet(data[0]); + + if (!this.identity.body.deviceId) + throw new Error('missing deviceId'); + + this._connection = await this._encryptClient(connection); + } catch (e) { + this.close(); + throw e; + } + } + + /** + * Negotiate an outgoing connection + * + * @param {Gio.SocketConnection} connection - The remote connection + */ + async open(connection) { + debug(`${this.address} (${this.uuid})`); + + try { + this._connection = connection; + this.backend.channels.set(this.address, this); + + await connection.get_output_stream().write_all_async( + this.backend.identity.serialize(), + GLib.PRIORITY_DEFAULT, + this.cancellable); + + this._connection = await this._encryptServer(connection); + } catch (e) { + this.close(); + throw e; + } + } + + /** + * Close all streams associated with this channel, silencing any errors + */ + close() { + if (this.closed) + return; + + debug(`${this.address} (${this.uuid})`); + this._closed = true; + this.notify('closed'); + + this.backend.channels.delete(this.address); + this.cancellable.cancel(); + + if (this._connection) + this._connection.close_async(GLib.PRIORITY_DEFAULT, null, null); + + if (this.input_stream) + this.input_stream.close_async(GLib.PRIORITY_DEFAULT, null, null); + + if (this.output_stream) + this.output_stream.close_async(GLib.PRIORITY_DEFAULT, null, null); + } + + async download(packet, target, cancellable = null) { + const address = Gio.InetSocketAddress.new_from_string(this.host, + packet.payloadTransferInfo.port); + + const client = new Gio.SocketClient({enable_proxy: false}); + const connection = await client.connect_async(address, cancellable) + .then(this._encryptClient.bind(this)); + + // Start the transfer + const transferredSize = await target.splice_async( + connection.input_stream, + (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | + Gio.OutputStreamSpliceFlags.CLOSE_TARGET), + GLib.PRIORITY_DEFAULT, cancellable); + + // If we get less than expected, we've certainly got corruption + if (transferredSize < packet.payloadSize) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.FAILED, + message: `Incomplete: ${transferredSize}/${packet.payloadSize}`, + }); + + // TODO: sometimes kdeconnect-android under-reports a file's size + // https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1157 + } else if (transferredSize > packet.payloadSize) { + logError(new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.FAILED, + message: `Extra Data: ${transferredSize - packet.payloadSize}`, + })); + } + } + + async upload(packet, source, size, cancellable = null) { + // Start listening on the first available port between 1739-1764 + const listener = new Gio.SocketListener(); + let port = TRANSFER_MIN; + + while (port <= TRANSFER_MAX) { + try { + listener.add_inet_port(port, null); + break; + } catch (e) { + if (port < TRANSFER_MAX) { + port++; + continue; + } else { + throw e; + } + } + } + + // Listen for the incoming connection + const acceptConnection = listener.accept_async(cancellable) + .then(result => this._encryptServer(result[0])); + + // Create an upload request + packet.body.payloadHash = this.checksum; + packet.payloadSize = size; + packet.payloadTransferInfo = {port: port}; + const requestUpload = this.sendPacket(new Core.Packet(packet), + cancellable); + + // Request an upload stream, accept the connection and get the output + const [, connection] = await Promise.all([requestUpload, + acceptConnection]); + + // Start the transfer + const transferredSize = await connection.output_stream.splice_async( + source, + (Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | + Gio.OutputStreamSpliceFlags.CLOSE_TARGET), + GLib.PRIORITY_DEFAULT, cancellable); + + if (transferredSize !== size) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.PARTIAL_INPUT, + message: 'Transfer incomplete', + }); + } + } + + async rejectTransfer(packet) { + try { + if (!packet || !packet.hasPayload()) + return; + + if (packet.payloadTransferInfo.port === undefined) + return; + + const address = Gio.InetSocketAddress.new_from_string(this.host, + packet.payloadTransferInfo.port); + + const client = new Gio.SocketClient({enable_proxy: false}); + const connection = await client.connect_async(address, null) + .then(this._encryptClient.bind(this)); + + connection.close_async(GLib.PRIORITY_DEFAULT, null, null); + } catch (e) { + debug(e, this.device.name); + } + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js new file mode 100644 index 0000000..1ed8cf6 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/__init__.js @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + + +/* + * Singleton Tracker + */ +const Default = new Map(); + + +/** + * Acquire a reference to a component. Calls to this function should always be + * followed by a call to `release()`. + * + * @param {string} name - The module name + * @return {*} The default instance of a component + */ +function acquire(name) { + let component; + + try { + let info = Default.get(name); + + if (info === undefined) { + const module = imports.service.components[name]; + + info = { + instance: new module.Component(), + refcount: 0, + }; + + Default.set(name, info); + } + + info.refcount++; + component = info.instance; + } catch (e) { + debug(e, name); + } + + return component; +} + + +/** + * Release a reference on a component. If the caller was the last reference + * holder, the component will be freed. + * + * @param {string} name - The module name + * @return {null} A %null value, useful for overriding a traced variable + */ +function release(name) { + try { + const info = Default.get(name); + + if (info.refcount === 1) { + info.instance.destroy(); + Default.delete(name); + } + + info.refcount--; + } catch (e) { + debug(e, name); + } + + return null; +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js new file mode 100644 index 0000000..33034e6 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/atspi.js @@ -0,0 +1,316 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +imports.gi.versions.Atspi = '2.0'; + +const Atspi = imports.gi.Atspi; +const Gdk = imports.gi.Gdk; + + +/** + * Printable ASCII range + */ +const _ASCII = /[\x20-\x7E]/; + + +/** + * Modifier Keycode Defaults + */ +const XKeycode = { + Alt_L: 0x40, + Control_L: 0x25, + Shift_L: 0x32, + Super_L: 0x85, +}; + + +/** + * A thin wrapper around Atspi for X11 sessions without Pipewire support. + */ +var Controller = class { + constructor() { + // Atspi.init() return 2 on fail, but still marks itself as inited. We + // uninit before throwing an error otherwise any future call to init() + // will appear successful and other calls will cause GSConnect to exit. + // See: https://gitlab.gnome.org/GNOME/at-spi2-core/blob/master/atspi/atspi-misc.c + if (Atspi.init() === 2) { + this.destroy(); + throw new Error('Failed to start AT-SPI'); + } + + try { + this._display = Gdk.Display.get_default(); + this._seat = this._display.get_default_seat(); + this._pointer = this._seat.get_pointer(); + } catch (e) { + this.destroy(); + throw e; + } + + // Try to read modifier keycodes from Gdk + try { + const keymap = Gdk.Keymap.get_for_display(this._display); + let modifier; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Alt_L)[1][0]; + XKeycode.Alt_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Control_L)[1][0]; + XKeycode.Control_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Shift_L)[1][0]; + XKeycode.Shift_L = modifier.keycode; + + modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0]; + XKeycode.Super_L = modifier.keycode; + } catch (e) { + debug('using default modifier keycodes'); + } + } + + /* + * Pointer events + */ + clickPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}c`); + } catch (e) { + logError(e); + } + } + + doubleclickPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}d`); + } catch (e) { + logError(e); + } + } + + movePointer(dx, dy) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * dx, scale * dy, 'rel'); + } catch (e) { + logError(e); + } + } + + pressPointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}p`); + } catch (e) { + logError(e); + } + } + + releasePointer(button) { + try { + const [, x, y] = this._pointer.get_position(); + const monitor = this._display.get_monitor_at_point(x, y); + const scale = monitor.get_scale_factor(); + Atspi.generate_mouse_event(scale * x, scale * y, `b${button}r`); + } catch (e) { + logError(e); + } + } + + scrollPointer(dx, dy) { + if (dy > 0) + this.clickPointer(4); + else if (dy < 0) + this.clickPointer(5); + } + + /* + * Phony virtual keyboard helpers + */ + _modeLock(keycode) { + Atspi.generate_keyboard_event( + keycode, + null, + Atspi.KeySynthType.PRESS + ); + } + + _modeUnlock(keycode) { + Atspi.generate_keyboard_event( + keycode, + null, + Atspi.KeySynthType.RELEASE + ); + } + + /* + * Simulate a printable-ASCII character. + * + */ + _pressASCII(key, modifiers) { + try { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeLock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeLock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeLock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeLock(XKeycode.Super_L); + + Atspi.generate_keyboard_event( + 0, + key, + Atspi.KeySynthType.STRING + ); + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeUnlock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeUnlock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeUnlock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeUnlock(XKeycode.Super_L); + } catch (e) { + logError(e); + } + } + + _pressKeysym(keysym, modifiers) { + try { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeLock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeLock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeLock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeLock(XKeycode.Super_L); + + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM + ); + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this._modeUnlock(XKeycode.Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this._modeUnlock(XKeycode.Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this._modeUnlock(XKeycode.Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this._modeUnlock(XKeycode.Super_L); + } catch (e) { + logError(e); + } + } + + /** + * Simulate the composition of a unicode character with: + * Control+Shift+u, [hex], Return + * + * @param {number} key - An XKeycode + * @param {number} modifiers - A modifier mask + */ + _pressUnicode(key, modifiers) { + try { + if (modifiers > 0) + log('GSConnect: ignoring modifiers for unicode keyboard event'); + + // TODO: Using Control and Shift keysym is not working (it triggers + // key release). Probably using LOCKMODIFIERS will not work either + // as unlocking the modifier will not trigger a release + + // Activate compose sequence + this._modeLock(XKeycode.Control_L); + this._modeLock(XKeycode.Shift_L); + + this.pressreleaseKeysym(Gdk.KEY_U); + + this._modeUnlock(XKeycode.Control_L); + this._modeUnlock(XKeycode.Shift_L); + + // Enter the unicode sequence + const ucode = key.charCodeAt(0).toString(16); + let keysym; + + for (let h = 0, len = ucode.length; h < len; h++) { + keysym = Gdk.unicode_to_keyval(ucode.charAt(h).codePointAt(0)); + this.pressreleaseKeysym(keysym); + } + + // Finish the compose sequence + this.pressreleaseKeysym(Gdk.KEY_Return); + } catch (e) { + logError(e); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESS | Atspi.KeySynthType.SYM + ); + } + + releaseKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.RELEASE | Atspi.KeySynthType.SYM + ); + } + + pressreleaseKeysym(keysym) { + Atspi.generate_keyboard_event( + keysym, + null, + Atspi.KeySynthType.PRESSRELEASE | Atspi.KeySynthType.SYM + ); + } + + pressKey(input, modifiers) { + // We were passed a keysym + if (typeof input === 'number') + this._pressKeysym(input, modifiers); + + // Regular ASCII + else if (_ASCII.test(input)) + this._pressASCII(input, modifiers); + + // Unicode + else + this._pressUnicode(input, modifiers); + } + + destroy() { + try { + Atspi.exit(); + } catch (e) { + // Silence errors + } + } +}; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js new file mode 100644 index 0000000..d51b9f7 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/clipboard.js @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; + + +const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard'; +const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard'; + + +var Clipboard = GObject.registerClass({ + GTypeName: 'GSConnectClipboard', + Properties: { + 'text': GObject.ParamSpec.string( + 'text', + 'Text Content', + 'The current text content of the clipboard', + GObject.ParamFlags.READWRITE, + '' + ), + }, +}, class Clipboard extends GObject.Object { + + _init() { + super._init(); + + this._cancellable = new Gio.Cancellable(); + this._clipboard = null; + + this._ownerChangeId = 0; + this._nameWatcherId = Gio.bus_watch_name( + Gio.BusType.SESSION, + DBUS_NAME, + Gio.BusNameWatcherFlags.NONE, + this._onNameAppeared.bind(this), + this._onNameVanished.bind(this) + ); + } + + get text() { + if (this._text === undefined) + this._text = ''; + + return this._text; + } + + set text(content) { + if (this.text === content) + return; + + this._text = content; + this.notify('text'); + + if (typeof content !== 'string') + return; + + if (this._clipboard instanceof Gtk.Clipboard) + this._clipboard.set_text(content, -1); + + if (this._clipboard instanceof Gio.DBusProxy) { + this._clipboard.call('SetText', new GLib.Variant('(s)', [content]), + Gio.DBusCallFlags.NO_AUTO_START, -1, this._cancellable) + .catch(debug); + } + } + + async _onNameAppeared(connection, name, name_owner) { + try { + // Cleanup the GtkClipboard + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._ownerChangeId = 0; + } + + // Create a proxy for the remote clipboard + this._clipboard = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: DBUS_NAME, + g_object_path: DBUS_PATH, + g_interface_name: DBUS_NAME, + g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, + }); + + await this._clipboard.init_async(GLib.PRIORITY_DEFAULT, + this._cancellable); + + this._ownerChangeId = this._clipboard.connect('g-signal', + this._onOwnerChange.bind(this)); + + this._onOwnerChange(); + if (!globalThis.HAVE_GNOME) { + // Directly subscrible signal + this.signalHandler = Gio.DBus.session.signal_subscribe( + DBUS_NAME, + DBUS_NAME, + 'OwnerChange', + DBUS_PATH, + null, + Gio.DBusSignalFlags.NONE, + this._onOwnerChange.bind(this) + ); + } + + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + debug(e); + this._onNameVanished(null, null); + } + } + } + + _onNameVanished(connection, name) { + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._clipboardChangedId = 0; + } + + const display = Gdk.Display.get_default(); + this._clipboard = Gtk.Clipboard.get_default(display); + + this._ownerChangeId = this._clipboard.connect('owner-change', + this._onOwnerChange.bind(this)); + + this._onOwnerChange(); + } + + async _onOwnerChange() { + try { + if (this._clipboard instanceof Gtk.Clipboard) + await this._gtkUpdateText(); + + else if (this._clipboard instanceof Gio.DBusProxy) + await this._proxyUpdateText(); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e); + } + } + + _applyUpdate(text) { + if (typeof text !== 'string' || this.text === text) + return; + + this._text = text; + this.notify('text'); + } + + /* + * Proxy Clipboard + */ + async _proxyUpdateText() { + let reply = await this._clipboard.call('GetMimetypes', null, + Gio.DBusCallFlags.NO_AUTO_START, -1, this._cancellable); + const mimetypes = reply.deepUnpack()[0]; + + // Special case for a cleared clipboard + if (mimetypes.length === 0) + return this._applyUpdate(''); + + // Special case to ignore copied files + if (mimetypes.includes('text/uri-list')) + return; + + reply = await this._clipboard.call('GetText', null, + Gio.DBusCallFlags.NO_AUTO_START, -1, this._cancellable); + const text = reply.deepUnpack()[0]; + + this._applyUpdate(text); + } + + /* + * GtkClipboard + */ + async _gtkUpdateText() { + const mimetypes = await new Promise((resolve, reject) => { + this._clipboard.request_targets((clipboard, atoms) => resolve(atoms)); + }); + + // Special case for a cleared clipboard + if (mimetypes.length === 0) + return this._applyUpdate(''); + + // Special case to ignore copied files + if (mimetypes.includes('text/uri-list')) + return; + + const text = await new Promise((resolve, reject) => { + this._clipboard.request_text((clipboard, text) => resolve(text)); + }); + + this._applyUpdate(text); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + + if (this._clipboard && this._ownerChangeId > 0) { + this._clipboard.disconnect(this._ownerChangeId); + this._ownerChangedId = 0; + } + + if (this._nameWatcherId > 0) { + Gio.bus_unwatch_name(this._nameWatcherId); + this._nameWatcherId = 0; + } + + if (!globalThis.HAVE_GNOME && this.signalHandler) + Gio.DBus.session.signal_unsubscribe(this.signalHandler); + } +}); + + +/** + * The service class for this component + */ +var Component = Clipboard; + +// vim:tabstop=2:shiftwidth=2:expandtab diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js new file mode 100644 index 0000000..9668596 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/contacts.js @@ -0,0 +1,619 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + +var HAVE_EDS = true; +var EBook = null; +var EBookContacts = null; +var EDataServer = null; + +try { + EBook = imports.gi.EBook; + EBookContacts = imports.gi.EBookContacts; + EDataServer = imports.gi.EDataServer; +} catch (e) { + HAVE_EDS = false; +} + + +/** + * A store for contacts + */ +var Store = GObject.registerClass({ + GTypeName: 'GSConnectContactsStore', + Properties: { + 'context': GObject.ParamSpec.string( + 'context', + 'Context', + 'Used as the cache directory, relative to Config.CACHEDIR', + GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlags.READWRITE, + null + ), + }, + Signals: { + 'contact-added': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + 'contact-removed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + 'contact-changed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + }, +}, class Store extends GObject.Object { + + _init(context = null) { + super._init({ + context: context, + }); + + this._cacheData = {}; + this._edsPrepared = false; + } + + /** + * Parse an EContact and add it to the store. + * + * @param {EBookContacts.Contact} econtact - an EContact to parse + * @param {string} [origin] - an optional origin string + */ + async _parseEContact(econtact, origin = 'desktop') { + try { + const contact = { + id: econtact.id, + name: _('Unknown Contact'), + numbers: [], + origin: origin, + timestamp: 0, + }; + + // Try to get a contact name + if (econtact.full_name) + contact.name = econtact.full_name; + + // Parse phone numbers + const nums = econtact.get_attributes(EBookContacts.ContactField.TEL); + + for (const attr of nums) { + const number = { + value: attr.get_value(), + type: 'unknown', + }; + + if (attr.has_type('CELL')) + number.type = 'cell'; + else if (attr.has_type('HOME')) + number.type = 'home'; + else if (attr.has_type('WORK')) + number.type = 'work'; + + contact.numbers.push(number); + } + + // Try and get a contact photo + const photo = econtact.photo; + + if (photo) { + if (photo.type === EBookContacts.ContactPhotoType.INLINED) { + const data = photo.get_inlined()[0]; + contact.avatar = await this.storeAvatar(data); + + } else if (photo.type === EBookContacts.ContactPhotoType.URI) { + const uri = econtact.photo.get_uri(); + contact.avatar = uri.replace('file://', ''); + } + } + + this.add(contact, false); + } catch (e) { + logError(e, `Failed to parse VCard contact ${econtact.id}`); + } + } + + /* + * AddressBook DBus callbacks + */ + _onObjectsAdded(connection, sender, path, iface, signal, params) { + try { + const adds = params.get_child_value(0).get_strv(); + + // NOTE: sequential pairs of vcard, id + for (let i = 0, len = adds.length; i < len; i += 2) { + try { + const vcard = adds[i]; + const econtact = EBookContacts.Contact.new_from_vcard(vcard); + this._parseEContact(econtact); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + _onObjectsRemoved(connection, sender, path, iface, signal, params) { + try { + const changes = params.get_child_value(0).get_strv(); + + for (const id of changes) { + try { + this.remove(id, false); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + _onObjectsModified(connection, sender, path, iface, signal, params) { + try { + const changes = params.get_child_value(0).get_strv(); + + // NOTE: sequential pairs of vcard, id + for (let i = 0, len = changes.length; i < len; i += 2) { + try { + const vcard = changes[i]; + const econtact = EBookContacts.Contact.new_from_vcard(vcard); + this._parseEContact(econtact); + } catch (e) { + debug(e); + } + } + } catch (e) { + debug(e); + } + } + + /* + * SourceRegistryWatcher callbacks + */ + async _onAppeared(watcher, source) { + try { + // Get an EBookClient and EBookView + const uid = source.get_uid(); + const client = await EBook.BookClient.connect(source, null); + const [view] = await client.get_view('exists "tel"', null); + + // Watch the view for changes to the address book + const connection = view.get_connection(); + const objectPath = view.get_object_path(); + + view._objectsAddedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsAdded', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsAdded.bind(this) + ); + + view._objectsRemovedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsRemoved', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsRemoved.bind(this) + ); + + view._objectsModifiedId = connection.signal_subscribe( + null, + 'org.gnome.evolution.dataserver.AddressBookView', + 'ObjectsModified', + objectPath, + null, + Gio.DBusSignalFlags.NONE, + this._onObjectsModified.bind(this) + ); + + view.start(); + + // Store the EBook in a map + this._ebooks.set(uid, { + source: source, + client: client, + view: view, + }); + } catch (e) { + debug(e); + } + } + + _onDisappeared(watcher, source) { + try { + const uid = source.get_uid(); + const ebook = this._ebooks.get(uid); + + if (ebook === undefined) + return; + + // Disconnect the EBookView + if (ebook.view) { + const connection = ebook.view.get_connection(); + connection.signal_unsubscribe(ebook.view._objectsAddedId); + connection.signal_unsubscribe(ebook.view._objectsRemovedId); + connection.signal_unsubscribe(ebook.view._objectsModifiedId); + + ebook.view.stop(); + } + + this._ebooks.delete(uid); + } catch (e) { + debug(e); + } + } + + async _initEvolutionDataServer() { + try { + if (this._edsPrepared) + return; + + this._edsPrepared = true; + this._ebooks = new Map(); + + // Get the current EBooks + const registry = await this._getESourceRegistry(); + + for (const source of registry.list_sources('Address Book')) + await this._onAppeared(null, source); + + // Watch for new and removed sources + this._watcher = new EDataServer.SourceRegistryWatcher({ + registry: registry, + extension_name: 'Address Book', + }); + + this._appearedId = this._watcher.connect( + 'appeared', + this._onAppeared.bind(this) + ); + this._disappearedId = this._watcher.connect( + 'disappeared', + this._onDisappeared.bind(this) + ); + } catch (e) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + } + + *[Symbol.iterator]() { + const contacts = Object.values(this._cacheData); + + for (let i = 0, len = contacts.length; i < len; i++) + yield contacts[i]; + } + + get contacts() { + return Object.values(this._cacheData); + } + + get context() { + if (this._context === undefined) + this._context = null; + + return this._context; + } + + set context(context) { + this._context = context; + this._cacheDir = Gio.File.new_for_path(Config.CACHEDIR); + + if (context !== null) + this._cacheDir = this._cacheDir.get_child(context); + + GLib.mkdir_with_parents(this._cacheDir.get_path(), 448); + this._cacheFile = this._cacheDir.get_child('contacts.json'); + } + + /** + * Save a Uint8Array to file and return the path + * + * @param {Uint8Array} contents - An image byte array + * @return {string|undefined} File path or %undefined on failure + */ + async storeAvatar(contents) { + const md5 = GLib.compute_checksum_for_data(GLib.ChecksumType.MD5, + contents); + const file = this._cacheDir.get_child(`${md5}`); + + if (!file.query_exists(null)) { + try { + await file.replace_contents_bytes_async( + new GLib.Bytes(contents), + null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); + } catch (e) { + debug(e, 'Storing avatar'); + return undefined; + } + } + + return file.get_path(); + } + + /** + * Query the Store for a contact by name and/or number. + * + * @param {Object} query - A query object + * @param {string} [query.name] - The contact's name + * @param {string} query.number - The contact's number + * @return {Object} A contact object + */ + query(query) { + // First look for an existing contact by number + const contacts = this.contacts; + const matches = []; + const qnumber = query.number.toPhoneNumber(); + + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + for (const num of contact.numbers) { + const cnumber = num.value.toPhoneNumber(); + + if (qnumber.endsWith(cnumber) || cnumber.endsWith(qnumber)) { + // If no query name or exact match, return immediately + if (!query.name || query.name === contact.name) + return contact; + + // Otherwise we might find an exact name match that shares + // the number with another contact + matches.push(contact); + } + } + } + + // Return the first match (pretty much what Android does) + if (matches.length > 0) + return matches[0]; + + // No match; return a mock contact with a unique ID + let id = GLib.uuid_string_random(); + + while (this._cacheData.hasOwnProperty(id)) + id = GLib.uuid_string_random(); + + return { + id: id, + name: query.name || query.number, + numbers: [{value: query.number, type: 'unknown'}], + origin: 'gsconnect', + }; + } + + get_contact(position) { + if (this._cacheData[position] !== undefined) + return this._cacheData[position]; + + return null; + } + + /** + * Add a contact, checking for validity + * + * @param {Object} contact - A contact object + * @param {boolean} write - Write to disk + */ + add(contact, write = true) { + // Ensure the contact has a unique id + if (!contact.id) { + let id = GLib.uuid_string_random(); + + while (this._cacheData[id]) + id = GLib.uuid_string_random(); + + contact.id = id; + } + + // Ensure the contact has an origin + if (!contact.origin) + contact.origin = 'gsconnect'; + + // This is an updated contact + if (this._cacheData[contact.id]) { + this._cacheData[contact.id] = contact; + this.emit('contact-changed', contact.id); + + // This is a new contact + } else { + this._cacheData[contact.id] = contact; + this.emit('contact-added', contact.id); + } + + // Write if requested + if (write) + this.save(); + } + + /** + * Remove a contact by id + * + * @param {string} id - The id of the contact to delete + * @param {boolean} write - Write to disk + */ + remove(id, write = true) { + // Only remove if the contact actually exists + if (this._cacheData[id]) { + delete this._cacheData[id]; + this.emit('contact-removed', id); + + // Write if requested + if (write) + this.save(); + } + } + + /** + * Lookup a contact for each address object in @addresses and return a + * dictionary of address (eg. phone number) to contact object. + * + * { "555-5555": { "name": "...", "numbers": [], ... } } + * + * @param {Object[]} addresses - A list of address objects + * @return {Object} A dictionary of phone numbers and contacts + */ + lookupAddresses(addresses) { + const contacts = {}; + + // Lookup contacts for each address + for (let i = 0, len = addresses.length; i < len; i++) { + const address = addresses[i].address; + + contacts[address] = this.query({ + number: address, + }); + } + + return contacts; + } + + async clear() { + try { + const contacts = this.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) + await this.remove(contacts[i].id, false); + + await this.save(); + } catch (e) { + debug(e); + } + } + + /** + * Update the contact store from a dictionary of our custom contact objects. + * + * @param {Object} json - an Object of contact Objects + */ + async update(json = {}) { + try { + let contacts = Object.values(json); + + for (let i = 0, len = contacts.length; i < len; i++) { + const new_contact = contacts[i]; + const contact = this._cacheData[new_contact.id]; + + if (!contact || new_contact.timestamp !== contact.timestamp) + await this.add(new_contact, false); + } + + // Prune contacts + contacts = this.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + if (!json[contact.id]) + await this.remove(contact.id, false); + } + + await this.save(); + } catch (e) { + debug(e, 'Updating contacts'); + } + } + + /** + * Fetch and update the contact store from its source. + * + * The default function initializes the EDS server, or logs a debug message + * if EDS is unavailable. Derived classes should request an update from the + * remote source. + */ + async fetch() { + try { + if (this.context === null && HAVE_EDS) + await this._initEvolutionDataServer(); + else + throw new Error('Evolution Data Server not available'); + } catch (e) { + debug(e); + } + } + + /** + * Load the contacts from disk. + */ + async load() { + try { + const [contents] = await this._cacheFile.load_contents_async(null); + this._cacheData = JSON.parse(new TextDecoder().decode(contents)); + } catch (e) { + debug(e); + } finally { + this.notify('context'); + } + } + + /** + * Save the contacts to disk. + */ + async save() { + // EDS is handling storage + if (this.context === null && HAVE_EDS) + return; + + if (this.__cache_lock) { + this.__cache_queue = true; + return; + } + + try { + this.__cache_lock = true; + + const contents = new GLib.Bytes(JSON.stringify(this._cacheData, null, 2)); + await this._cacheFile.replace_contents_bytes_async(contents, null, + false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); + } catch (e) { + debug(e); + } finally { + this.__cache_lock = false; + + if (this.__cache_queue) { + this.__cache_queue = false; + this.save(); + } + } + } + + destroy() { + if (this._watcher !== undefined) { + this._watcher.disconnect(this._appearedId); + this._watcher.disconnect(this._disappearedId); + this._watcher = undefined; + + for (const ebook of this._ebooks.values()) + this._onDisappeared(null, ebook.source); + + this._edsPrepared = false; + } + } +}); + + +/** + * The service class for this component + */ +var Component = Store; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/input.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/input.js new file mode 100644 index 0000000..47540d8 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/input.js @@ -0,0 +1,580 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +const SESSION_TIMEOUT = 15; + + +const RemoteSession = GObject.registerClass({ + GTypeName: 'GSConnectRemoteSession', + Implements: [Gio.DBusInterface], + Signals: { + 'closed': { + flags: GObject.SignalFlags.RUN_FIRST, + }, + }, +}, class RemoteSession extends Gio.DBusProxy { + + _init(objectPath) { + super._init({ + g_bus_type: Gio.BusType.SESSION, + g_name: 'org.gnome.Mutter.RemoteDesktop', + g_object_path: objectPath, + g_interface_name: 'org.gnome.Mutter.RemoteDesktop.Session', + g_flags: Gio.DBusProxyFlags.NONE, + }); + + this._started = false; + } + + vfunc_g_signal(sender_name, signal_name, parameters) { + if (signal_name === 'Closed') + this.emit('closed'); + } + + _call(name, parameters = null) { + if (!this._started) + return; + + // Pass a null callback to allow this call to finish itself + this.call(name, parameters, Gio.DBusCallFlags.NONE, -1, null, null); + } + + get session_id() { + try { + return this.get_cached_property('SessionId').unpack(); + } catch (e) { + return null; + } + } + + async start() { + try { + if (this._started) + return; + + // Initialize the proxy, and start the session + await this.init_async(GLib.PRIORITY_DEFAULT, null); + await this.call('Start', null, Gio.DBusCallFlags.NONE, -1, null); + + this._started = true; + } catch (e) { + this.destroy(); + + Gio.DBusError.strip_remote_error(e); + throw e; + } + } + + stop() { + if (this._started) { + this._started = false; + + // Pass a null callback to allow this call to finish itself + this.call('Stop', null, Gio.DBusCallFlags.NONE, -1, null, null); + } + } + + _translateButton(button) { + switch (button) { + case Gdk.BUTTON_PRIMARY: + return 0x110; + + case Gdk.BUTTON_MIDDLE: + return 0x112; + + case Gdk.BUTTON_SECONDARY: + return 0x111; + + case 4: + return 0; // FIXME + + case 5: + return 0x10F; // up + } + } + + movePointer(dx, dy) { + this._call( + 'NotifyPointerMotionRelative', + GLib.Variant.new('(dd)', [dx, dy]) + ); + } + + pressPointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, true]) + ); + } + + releasePointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, false]) + ); + } + + clickPointer(button) { + button = this._translateButton(button); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, true]) + ); + + this._call( + 'NotifyPointerButton', + GLib.Variant.new('(ib)', [button, false]) + ); + } + + doubleclickPointer(button) { + this.clickPointer(button); + this.clickPointer(button); + } + + scrollPointer(dx, dy) { + // NOTE: NotifyPointerAxis only seems to work on Wayland, but maybe + // NotifyPointerAxisDiscrete is the better choice anyways + if (HAVE_WAYLAND) { + this._call( + 'NotifyPointerAxis', + GLib.Variant.new('(ddu)', [dx, dy, 0]) + ); + this._call( + 'NotifyPointerAxis', + GLib.Variant.new('(ddu)', [0, 0, 1]) + ); + } else if (dy > 0) { + this._call( + 'NotifyPointerAxisDiscrete', + GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, 1]) + ); + } else if (dy < 0) { + this._call( + 'NotifyPointerAxisDiscrete', + GLib.Variant.new('(ui)', [Gdk.ScrollDirection.UP, -1]) + ); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, true]) + ); + } + + releaseKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, false]) + ); + } + + pressreleaseKeysym(keysym) { + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, true]) + ); + this._call( + 'NotifyKeyboardKeysym', + GLib.Variant.new('(ub)', [keysym, false]) + ); + } + + /* + * High-level keyboard input + */ + pressKey(input, modifiers) { + // Press Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this.pressKeysym(Gdk.KEY_Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this.pressKeysym(Gdk.KEY_Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this.pressKeysym(Gdk.KEY_Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this.pressKeysym(Gdk.KEY_Super_L); + + if (typeof input === 'string') { + const keysym = Gdk.unicode_to_keyval(input.codePointAt(0)); + this.pressreleaseKeysym(keysym); + } else { + this.pressreleaseKeysym(input); + } + + // Release Modifiers + if (modifiers & Gdk.ModifierType.MOD1_MASK) + this.releaseKeysym(Gdk.KEY_Alt_L); + if (modifiers & Gdk.ModifierType.CONTROL_MASK) + this.releaseKeysym(Gdk.KEY_Control_L); + if (modifiers & Gdk.ModifierType.SHIFT_MASK) + this.releaseKeysym(Gdk.KEY_Shift_L); + if (modifiers & Gdk.ModifierType.SUPER_MASK) + this.releaseKeysym(Gdk.KEY_Super_L); + } + + destroy() { + if (this.__disposed === undefined) { + this.__disposed = true; + GObject.signal_handlers_destroy(this); + } + } +}); + + +class Controller { + constructor() { + this._nameAppearedId = 0; + this._session = null; + this._sessionCloseId = 0; + this._sessionExpiry = 0; + this._sessionExpiryId = 0; + this._sessionStarting = false; + + // Watch for the RemoteDesktop portal + this._nameWatcherId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.Mutter.RemoteDesktop', + Gio.BusNameWatcherFlags.NONE, + this._onNameAppeared.bind(this), + this._onNameVanished.bind(this) + ); + } + + get connection() { + if (this._connection === undefined) + this._connection = null; + + return this._connection; + } + + /** + * Check if this is a Wayland session, specifically for distributions that + * don't ship pipewire support (eg. Debian/Ubuntu). + * + * FIXME: this is a super ugly hack that should go away + * + * @return {boolean} %true if wayland is not supported + */ + _checkWayland() { + if (HAVE_WAYLAND) { + // eslint-disable-next-line no-global-assign + HAVE_REMOTEINPUT = false; + const service = Gio.Application.get_default(); + + if (service === null) + return true; + + // First we're going to disabled the affected plugins on all devices + for (const device of service.manager.devices.values()) { + const supported = device.settings.get_strv('supported-plugins'); + let index; + + if ((index = supported.indexOf('mousepad')) > -1) + supported.splice(index, 1); + + if ((index = supported.indexOf('presenter')) > -1) + supported.splice(index, 1); + + device.settings.set_strv('supported-plugins', supported); + } + + // Second we need each backend to rebuild its identity packet and + // broadcast the amended capabilities to the network + for (const backend of service.manager.backends.values()) + backend.buildIdentity(); + + service.manager.identify(); + + return true; + } + + return false; + } + + _onNameAppeared(connection, name, name_owner) { + try { + this._connection = connection; + } catch (e) { + logError(e); + } + } + + _onNameVanished(connection, name) { + try { + if (this._session !== null) + this._onSessionClosed(this._session); + } catch (e) { + logError(e); + } + } + + _onSessionClosed(session) { + // Disconnect from the session + if (this._sessionClosedId > 0) { + session.disconnect(this._sessionClosedId); + this._sessionClosedId = 0; + } + + // Destroy the session + session.destroy(); + this._session = null; + } + + _onSessionExpired() { + // If the session has been used recently, schedule a new expiry + const remainder = Math.floor(this._sessionExpiry - (Date.now() / 1000)); + + if (remainder > 0) { + this._sessionExpiryId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + remainder, + this._onSessionExpired.bind(this) + ); + + return GLib.SOURCE_REMOVE; + } + + // Otherwise if there's an active session, close it + if (this._session !== null) + this._session.stop(); + + // Reset the GSource Id + this._sessionExpiryId = 0; + + return GLib.SOURCE_REMOVE; + } + + async _createRemoteDesktopSession() { + if (this.connection === null) + return Promise.reject(new Error('No DBus connection')); + + const reply = await this.connection.call( + 'org.gnome.Mutter.RemoteDesktop', + '/org/gnome/Mutter/RemoteDesktop', + 'org.gnome.Mutter.RemoteDesktop', + 'CreateSession', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null); + + return reply.deepUnpack()[0]; + } + + async _ensureAdapter() { + try { + // Update the timestamp of the last event + this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT); + + // Session is active + if (this._session !== null) + return; + + // Mutter's RemoteDesktop is not available, fall back to Atspi + if (this.connection === null) { + debug('Falling back to Atspi'); + + // If we got here in Wayland, we need to re-adjust and bail + if (this._checkWayland()) + return; + + const fallback = imports.service.components.atspi; + this._session = new fallback.Controller(); + + // Mutter is available and there isn't another session starting + } else if (this._sessionStarting === false) { + this._sessionStarting = true; + + debug('Creating Mutter RemoteDesktop session'); + + // This takes three steps: creating the remote desktop session, + // starting the session, and creating a screencast session for + // the remote desktop session. + const objectPath = await this._createRemoteDesktopSession(); + + this._session = new RemoteSession(objectPath); + await this._session.start(); + + // Watch for the session ending + this._sessionClosedId = this._session.connect( + 'closed', + this._onSessionClosed.bind(this) + ); + + if (this._sessionExpiryId === 0) { + this._sessionExpiryId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + SESSION_TIMEOUT, + this._onSessionExpired.bind(this) + ); + } + + this._sessionStarting = false; + } + } catch (e) { + logError(e); + + if (this._session !== null) { + this._session.destroy(); + this._session = null; + } + + this._sessionStarting = false; + } + } + + /* + * Pointer Events + */ + movePointer(dx, dy) { + try { + if (dx === 0 && dy === 0) + return; + + this._ensureAdapter(); + this._session.movePointer(dx, dy); + } catch (e) { + debug(e); + } + } + + pressPointer(button) { + try { + this._ensureAdapter(); + this._session.pressPointer(button); + } catch (e) { + debug(e); + } + } + + releasePointer(button) { + try { + this._ensureAdapter(); + this._session.releasePointer(button); + } catch (e) { + debug(e); + } + } + + clickPointer(button) { + try { + this._ensureAdapter(); + this._session.clickPointer(button); + } catch (e) { + debug(e); + } + } + + doubleclickPointer(button) { + try { + this._ensureAdapter(); + this._session.doubleclickPointer(button); + } catch (e) { + debug(e); + } + } + + scrollPointer(dx, dy) { + if (dx === 0 && dy === 0) + return; + + try { + this._ensureAdapter(); + this._session.scrollPointer(dx, dy); + } catch (e) { + debug(e); + } + } + + /* + * Keyboard Events + */ + pressKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.pressKeysym(keysym); + } catch (e) { + debug(e); + } + } + + releaseKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.releaseKeysym(keysym); + } catch (e) { + debug(e); + } + } + + pressreleaseKeysym(keysym) { + try { + this._ensureAdapter(); + this._session.pressreleaseKeysym(keysym); + } catch (e) { + debug(e); + } + } + + /* + * High-level keyboard input + */ + pressKeys(input, modifiers) { + try { + this._ensureAdapter(); + + if (typeof input === 'string') { + for (let i = 0; i < input.length; i++) + this._session.pressKey(input[i], modifiers); + } else { + this._session.pressKey(input, modifiers); + } + } catch (e) { + debug(e); + } + } + + destroy() { + if (this._session !== null) { + // Disconnect from the session + if (this._sessionClosedId > 0) { + this._session.disconnect(this._sessionClosedId); + this._sessionClosedId = 0; + } + + this._session.destroy(); + this._session = null; + } + + if (this._nameWatcherId > 0) { + Gio.bus_unwatch_name(this._nameWatcherId); + this._nameWatcherId = 0; + } + } +} + + +/** + * The service class for this component + */ +var Component = Controller; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js new file mode 100644 index 0000000..df9adaf --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/mpris.js @@ -0,0 +1,1005 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +var Player = GObject.registerClass({ + GTypeName: 'GSConnectMediaPlayerInterface', + Properties: { + // Application Properties + 'CanQuit': GObject.ParamSpec.boolean( + 'CanQuit', + 'Can Quit', + 'Whether the client can call the Quit method.', + GObject.ParamFlags.READABLE, + false + ), + 'Fullscreen': GObject.ParamSpec.boolean( + 'Fullscreen', + 'Fullscreen', + 'Whether the player is in fullscreen mode.', + GObject.ParamFlags.READWRITE, + false + ), + 'CanSetFullscreen': GObject.ParamSpec.boolean( + 'CanSetFullscreen', + 'Can Set Fullscreen', + 'Whether the client can set the Fullscreen property.', + GObject.ParamFlags.READABLE, + false + ), + 'CanRaise': GObject.ParamSpec.boolean( + 'CanRaise', + 'Can Raise', + 'Whether the client can call the Raise method.', + GObject.ParamFlags.READABLE, + false + ), + 'HasTrackList': GObject.ParamSpec.boolean( + 'HasTrackList', + 'Has Track List', + 'Whether the player has a track list.', + GObject.ParamFlags.READABLE, + false + ), + 'Identity': GObject.ParamSpec.string( + 'Identity', + 'Identity', + 'The application name.', + GObject.ParamFlags.READABLE, + null + ), + 'DesktopEntry': GObject.ParamSpec.string( + 'DesktopEntry', + 'DesktopEntry', + 'The basename of an installed .desktop file.', + GObject.ParamFlags.READABLE, + null + ), + 'SupportedUriSchemes': GObject.param_spec_variant( + 'SupportedUriSchemes', + 'Supported URI Schemes', + 'The URI schemes supported by the media player.', + new GLib.VariantType('as'), + null, + GObject.ParamFlags.READABLE + ), + 'SupportedMimeTypes': GObject.param_spec_variant( + 'SupportedMimeTypes', + 'Supported MIME Types', + 'The mime-types supported by the media player.', + new GLib.VariantType('as'), + null, + GObject.ParamFlags.READABLE + ), + + // Player Properties + 'PlaybackStatus': GObject.ParamSpec.string( + 'PlaybackStatus', + 'Playback Status', + 'The current playback status.', + GObject.ParamFlags.READABLE, + null + ), + 'LoopStatus': GObject.ParamSpec.string( + 'LoopStatus', + 'Loop Status', + 'The current loop status.', + GObject.ParamFlags.READWRITE, + null + ), + 'Rate': GObject.ParamSpec.double( + 'Rate', + 'Rate', + 'The current playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'MinimumRate': GObject.ParamSpec.double( + 'MinimumRate', + 'Minimum Rate', + 'The minimum playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'MaximimRate': GObject.ParamSpec.double( + 'MaximumRate', + 'Maximum Rate', + 'The maximum playback rate.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'Shuffle': GObject.ParamSpec.boolean( + 'Shuffle', + 'Shuffle', + 'Whether track changes are linear.', + GObject.ParamFlags.READWRITE, + null + ), + 'Metadata': GObject.param_spec_variant( + 'Metadata', + 'Metadata', + 'The metadata of the current element.', + new GLib.VariantType('a{sv}'), + null, + GObject.ParamFlags.READABLE + ), + 'Volume': GObject.ParamSpec.double( + 'Volume', + 'Volume', + 'The volume level.', + GObject.ParamFlags.READWRITE, + 0.0, 1.0, + 1.0 + ), + 'Position': GObject.ParamSpec.int64( + 'Position', + 'Position', + 'The current track position in microseconds.', + GObject.ParamFlags.READABLE, + 0, Number.MAX_SAFE_INTEGER, + 0 + ), + 'CanGoNext': GObject.ParamSpec.boolean( + 'CanGoNext', + 'Can Go Next', + 'Whether the client can call the Next method.', + GObject.ParamFlags.READABLE, + false + ), + 'CanGoPrevious': GObject.ParamSpec.boolean( + 'CanGoPrevious', + 'Can Go Previous', + 'Whether the client can call the Previous method.', + GObject.ParamFlags.READABLE, + false + ), + 'CanPlay': GObject.ParamSpec.boolean( + 'CanPlay', + 'Can Play', + 'Whether playback can be started using Play or PlayPause.', + GObject.ParamFlags.READABLE, + false + ), + 'CanPause': GObject.ParamSpec.boolean( + 'CanPause', + 'Can Pause', + 'Whether playback can be paused using Play or PlayPause.', + GObject.ParamFlags.READABLE, + false + ), + 'CanSeek': GObject.ParamSpec.boolean( + 'CanSeek', + 'Can Seek', + 'Whether the client can control the playback position using Seek and SetPosition.', + GObject.ParamFlags.READABLE, + false + ), + 'CanControl': GObject.ParamSpec.boolean( + 'CanControl', + 'Can Control', + 'Whether the media player may be controlled over this interface.', + GObject.ParamFlags.READABLE, + false + ), + }, + Signals: { + 'Seeked': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_INT64], + }, + }, +}, class Player extends GObject.Object { + + /* + * The org.mpris.MediaPlayer2 Interface + */ + get CanQuit() { + if (this._CanQuit === undefined) + this._CanQuit = false; + + return this._CanQuit; + } + + get CanRaise() { + if (this._CanRaise === undefined) + this._CanRaise = false; + + return this._CanRaise; + } + + get CanSetFullscreen() { + if (this._CanFullscreen === undefined) + this._CanFullscreen = false; + + return this._CanFullscreen; + } + + get DesktopEntry() { + if (this._DesktopEntry === undefined) + return 'org.gnome.Shell.Extensions.GSConnect'; + + return this._DesktopEntry; + } + + get Fullscreen() { + if (this._Fullscreen === undefined) + this._Fullscreen = false; + + return this._Fullscreen; + } + + set Fullscreen(mode) { + if (this.Fullscreen === mode) + return; + + this._Fullscreen = mode; + this.notify('Fullscreen'); + } + + get HasTrackList() { + if (this._HasTrackList === undefined) + this._HasTrackList = false; + + return this._HasTrackList; + } + + get Identity() { + if (this._Identity === undefined) + this._Identity = ''; + + return this._Identity; + } + + get SupportedMimeTypes() { + if (this._SupportedMimeTypes === undefined) + this._SupportedMimeTypes = []; + + return this._SupportedMimeTypes; + } + + get SupportedUriSchemes() { + if (this._SupportedUriSchemes === undefined) + this._SupportedUriSchemes = []; + + return this._SupportedUriSchemes; + } + + Quit() { + throw new GObject.NotImplementedError(); + } + + Raise() { + throw new GObject.NotImplementedError(); + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + if (this._CanControl === undefined) + this._CanControl = false; + + return this._CanControl; + } + + get CanGoNext() { + if (this._CanGoNext === undefined) + this._CanGoNext = false; + + return this._CanGoNext; + } + + get CanGoPrevious() { + if (this._CanGoPrevious === undefined) + this._CanGoPrevious = false; + + return this._CanGoPrevious; + } + + get CanPause() { + if (this._CanPause === undefined) + this._CanPause = false; + + return this._CanPause; + } + + get CanPlay() { + if (this._CanPlay === undefined) + this._CanPlay = false; + + return this._CanPlay; + } + + get CanSeek() { + if (this._CanSeek === undefined) + this._CanSeek = false; + + return this._CanSeek; + } + + get LoopStatus() { + if (this._LoopStatus === undefined) + this._LoopStatus = 'None'; + + return this._LoopStatus; + } + + set LoopStatus(status) { + if (this.LoopStatus === status) + return; + + this._LoopStatus = status; + this.notify('LoopStatus'); + } + + get MaximumRate() { + if (this._MaximumRate === undefined) + this._MaximumRate = 1.0; + + return this._MaximumRate; + } + + get Metadata() { + if (this._Metadata === undefined) { + this._Metadata = { + 'xesam:artist': [_('Unknown')], + 'xesam:album': _('Unknown'), + 'xesam:title': _('Unknown'), + 'mpris:length': 0, + }; + } + + return this._Metadata; + } + + get MinimumRate() { + if (this._MinimumRate === undefined) + this._MinimumRate = 1.0; + + return this._MinimumRate; + } + + get PlaybackStatus() { + if (this._PlaybackStatus === undefined) + this._PlaybackStatus = 'Stopped'; + + return this._PlaybackStatus; + } + + get Position() { + if (this._Position === undefined) + this._Position = 0; + + return this._Position; + } + + get Rate() { + if (this._Rate === undefined) + this._Rate = 1.0; + + return this._Rate; + } + + set Rate(rate) { + if (this.Rate === rate) + return; + + this._Rate = rate; + this.notify('Rate'); + } + + get Shuffle() { + if (this._Shuffle === undefined) + this._Shuffle = false; + + return this._Shuffle; + } + + set Shuffle(mode) { + if (this.Shuffle === mode) + return; + + this._Shuffle = mode; + this.notify('Shuffle'); + } + + get Volume() { + if (this._Volume === undefined) + this._Volume = 1.0; + + return this._Volume; + } + + set Volume(level) { + if (this.Volume === level) + return; + + this._Volume = level; + this.notify('Volume'); + } + + Next() { + throw new GObject.NotImplementedError(); + } + + OpenUri(uri) { + throw new GObject.NotImplementedError(); + } + + Previous() { + throw new GObject.NotImplementedError(); + } + + Pause() { + throw new GObject.NotImplementedError(); + } + + Play() { + throw new GObject.NotImplementedError(); + } + + PlayPause() { + throw new GObject.NotImplementedError(); + } + + Seek(offset) { + throw new GObject.NotImplementedError(); + } + + SetPosition(trackId, position) { + throw new GObject.NotImplementedError(); + } + + Stop() { + throw new GObject.NotImplementedError(); + } +}); + + +/** + * An aggregate of the org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player + * interfaces. + */ +const PlayerProxy = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlayer', +}, class PlayerProxy extends Player { + + _init(name) { + super._init(); + + this._application = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: name, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_name: 'org.mpris.MediaPlayer2', + }); + + this._applicationChangedId = this._application.connect( + 'g-properties-changed', + this._onPropertiesChanged.bind(this) + ); + + this._player = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SESSION, + g_name: name, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_name: 'org.mpris.MediaPlayer2.Player', + }); + + this._playerChangedId = this._player.connect( + 'g-properties-changed', + this._onPropertiesChanged.bind(this) + ); + + this._playerSignalId = this._player.connect( + 'g-signal', + this._onSignal.bind(this) + ); + + this._cancellable = new Gio.Cancellable(); + } + + _onSignal(proxy, sender_name, signal_name, parameters) { + try { + if (signal_name !== 'Seeked') + return; + + this.emit('Seeked', parameters.deepUnpack()[0]); + } catch (e) { + debug(e, proxy.g_name); + } + } + + _call(proxy, name, parameters = null) { + proxy.call( + name, + parameters, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, result) => { + try { + proxy.call_finish(result); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + debug(e, proxy.g_name); + } + } + ); + } + + _get(proxy, name, fallback = null) { + try { + return proxy.get_cached_property(name).recursiveUnpack(); + } catch (e) { + return fallback; + } + } + + _set(proxy, name, value) { + try { + proxy.set_cached_property(name, value); + + proxy.call( + 'org.freedesktop.DBus.Properties.Set', + new GLib.Variant('(ssv)', [proxy.g_interface_name, name, value]), + Gio.DBusCallFlags.NO_AUTO_START, + -1, + this._cancellable, + (proxy, result) => { + try { + proxy.call_finish(result); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + debug(e, proxy.g_name); + } + } + ); + } catch (e) { + debug(e, proxy.g_name); + } + } + + _onPropertiesChanged(proxy, changed, invalidated) { + try { + this.freeze_notify(); + + for (const name in changed.deepUnpack()) + this.notify(name); + + this.thaw_notify(); + } catch (e) { + debug(e, proxy.g_name); + } + } + + /* + * The org.mpris.MediaPlayer2 Interface + */ + get CanQuit() { + return this._get(this._application, 'CanQuit', false); + } + + get CanRaise() { + return this._get(this._application, 'CanRaise', false); + } + + get CanSetFullscreen() { + return this._get(this._application, 'CanSetFullscreen', false); + } + + get DesktopEntry() { + return this._get(this._application, 'DesktopEntry', null); + } + + get Fullscreen() { + return this._get(this._application, 'Fullscreen', false); + } + + set Fullscreen(mode) { + this._set(this._application, 'Fullscreen', new GLib.Variant('b', mode)); + } + + get HasTrackList() { + return this._get(this._application, 'HasTrackList', false); + } + + get Identity() { + return this._get(this._application, 'Identity', _('Unknown')); + } + + get SupportedMimeTypes() { + return this._get(this._application, 'SupportedMimeTypes', []); + } + + get SupportedUriSchemes() { + return this._get(this._application, 'SupportedUriSchemes', []); + } + + Quit() { + this._call(this._application, 'Quit'); + } + + Raise() { + this._call(this._application, 'Raise'); + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + return this._get(this._player, 'CanControl', false); + } + + get CanGoNext() { + return this._get(this._player, 'CanGoNext', false); + } + + get CanGoPrevious() { + return this._get(this._player, 'CanGoPrevious', false); + } + + get CanPause() { + return this._get(this._player, 'CanPause', false); + } + + get CanPlay() { + return this._get(this._player, 'CanPlay', false); + } + + get CanSeek() { + return this._get(this._player, 'CanSeek', false); + } + + get LoopStatus() { + return this._get(this._player, 'LoopStatus', 'None'); + } + + set LoopStatus(status) { + this._set(this._player, 'LoopStatus', new GLib.Variant('s', status)); + } + + get MaximumRate() { + return this._get(this._player, 'MaximumRate', 1.0); + } + + get Metadata() { + if (this._metadata === undefined) { + this._metadata = { + 'xesam:artist': [_('Unknown')], + 'xesam:album': _('Unknown'), + 'xesam:title': _('Unknown'), + 'mpris:length': 0, + }; + } + + return this._get(this._player, 'Metadata', this._metadata); + } + + get MinimumRate() { + return this._get(this._player, 'MinimumRate', 1.0); + } + + get PlaybackStatus() { + return this._get(this._player, 'PlaybackStatus', 'Stopped'); + } + + // g-properties-changed is not emitted for this property + get Position() { + try { + const reply = this._player.call_sync( + 'org.freedesktop.DBus.Properties.Get', + new GLib.Variant('(ss)', [ + 'org.mpris.MediaPlayer2.Player', + 'Position', + ]), + Gio.DBusCallFlags.NONE, + -1, + null + ); + + return reply.recursiveUnpack()[0]; + } catch (e) { + return 0; + } + } + + get Rate() { + return this._get(this._player, 'Rate', 1.0); + } + + set Rate(rate) { + this._set(this._player, 'Rate', new GLib.Variant('d', rate)); + } + + get Shuffle() { + return this._get(this._player, 'Shuffle', false); + } + + set Shuffle(mode) { + this._set(this._player, 'Shuffle', new GLib.Variant('b', mode)); + } + + get Volume() { + return this._get(this._player, 'Volume', 1.0); + } + + set Volume(level) { + this._set(this._player, 'Volume', new GLib.Variant('d', level)); + } + + Next() { + this._call(this._player, 'Next'); + } + + OpenUri(uri) { + this._call(this._player, 'OpenUri', new GLib.Variant('(s)', [uri])); + } + + Previous() { + this._call(this._player, 'Previous'); + } + + Pause() { + this._call(this._player, 'Pause'); + } + + Play() { + this._call(this._player, 'Play'); + } + + PlayPause() { + this._call(this._player, 'PlayPause'); + } + + Seek(offset) { + this._call(this._player, 'Seek', new GLib.Variant('(x)', [offset])); + } + + SetPosition(trackId, position) { + this._call(this._player, 'SetPosition', + new GLib.Variant('(ox)', [trackId, position])); + } + + Stop() { + this._call(this._player, 'Stop'); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + this._application.disconnect(this._applicationChangedId); + this._player.disconnect(this._playerChangedId); + this._player.disconnect(this._playerSignalId); + } +}); + + +/** + * A manager for media players + */ +var Manager = GObject.registerClass({ + GTypeName: 'GSConnectMPRISManager', + Signals: { + 'player-added': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-removed': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-changed': { + param_types: [GObject.TYPE_OBJECT], + }, + 'player-seeked': { + param_types: [GObject.TYPE_OBJECT, GObject.TYPE_INT64], + }, + }, +}, class Manager extends GObject.Object { + + _init() { + super._init(); + + // Asynchronous setup + this._cancellable = new Gio.Cancellable(); + this._connection = Gio.DBus.session; + this._players = new Map(); + this._paused = new Map(); + + this._nameOwnerChangedId = Gio.DBus.session.signal_subscribe( + 'org.freedesktop.DBus', + 'org.freedesktop.DBus', + 'NameOwnerChanged', + '/org/freedesktop/DBus', + 'org.mpris.MediaPlayer2', + Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE, + this._onNameOwnerChanged.bind(this) + ); + + this._loadPlayers(); + } + + async _loadPlayers() { + try { + const reply = await this._connection.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'ListNames', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + this._cancellable); + + const names = reply.deepUnpack()[0]; + + for (let i = 0, len = names.length; i < len; i++) { + const name = names[i]; + + if (!name.startsWith('org.mpris.MediaPlayer2')) + continue; + + if (!name.includes('GSConnect')) + this._addPlayer(name); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } + } + + _onNameOwnerChanged(connection, sender, object, iface, signal, parameters) { + const [name, oldOwner, newOwner] = parameters.deepUnpack(); + + if (name.includes('GSConnect')) + return; + + if (newOwner.length) + this._addPlayer(name); + else if (oldOwner.length) + this._removePlayer(name); + } + + async _addPlayer(name) { + try { + if (!this._players.has(name)) { + const player = new PlayerProxy(name); + await Promise.all([ + player._application.init_async(GLib.PRIORITY_DEFAULT, + this._cancellable), + player._player.init_async(GLib.PRIORITY_DEFAULT, + this._cancellable), + ]); + + player.connect('notify', + (player) => this.emit('player-changed', player)); + + player.connect('Seeked', this.emit.bind(this, 'player-seeked')); + + this._players.set(name, player); + this.emit('player-added', player); + } + } catch (e) { + debug(e, name); + } + } + + _removePlayer(name) { + try { + const player = this._players.get(name); + + if (player !== undefined) { + this._paused.delete(name); + this._players.delete(name); + this.emit('player-removed', player); + + player.destroy(); + } + } catch (e) { + debug(e, name); + } + } + + /** + * Check for a player by its Identity. + * + * @param {string} identity - A player name + * @return {boolean} %true if the player was found + */ + hasPlayer(identity) { + for (const player of this._players.values()) { + if (player.Identity === identity) + return true; + } + + return false; + } + + /** + * Get a player by its Identity. + * + * @param {string} identity - A player name + * @return {GSConnectMPRISPlayer|null} A player or %null + */ + getPlayer(identity) { + for (const player of this._players.values()) { + if (player.Identity === identity) + return player; + } + + return null; + } + + /** + * Get a list of player identities. + * + * @return {string[]} A list of player identities + */ + getIdentities() { + const identities = []; + + for (const player of this._players.values()) { + const identity = player.Identity; + + if (identity) + identities.push(identity); + } + + return identities; + } + + /** + * A convenience function for pausing all players currently playing. + */ + pauseAll() { + for (const [name, player] of this._players) { + if (player.PlaybackStatus === 'Playing' && player.CanPause) { + player.Pause(); + this._paused.set(name, player); + } + } + } + + /** + * A convenience function for restarting all players paused with pauseAll(). + */ + unpauseAll() { + for (const player of this._paused.values()) { + if (player.PlaybackStatus === 'Paused' && player.CanPlay) + player.Play(); + } + + this._paused.clear(); + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + this._connection.signal_unsubscribe(this._nameOwnerChangedId); + + this._paused.clear(); + this._players.forEach(player => player.destroy()); + this._players.clear(); + } +}); + + +/** + * The service class for this component + */ +var Component = Manager; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/notification.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/notification.js new file mode 100644 index 0000000..17b31d6 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/notification.js @@ -0,0 +1,411 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GjsPrivate = imports.gi.GjsPrivate; +const GObject = imports.gi.GObject; + +const DBus = imports.service.utils.dbus; + + +const _nodeInfo = Gio.DBusNodeInfo.new_for_xml(` + + + + + + + + + + + + + + + + + + + + + + + + + +`); + + +const FDO_IFACE = _nodeInfo.lookup_interface('org.freedesktop.Notifications'); +const FDO_MATCH = "interface='org.freedesktop.Notifications',member='Notify',type='method_call'"; + +const GTK_IFACE = _nodeInfo.lookup_interface('org.gtk.Notifications'); +const GTK_MATCH = "interface='org.gtk.Notifications',member='AddNotification',type='method_call'"; + + +/** + * A class for snooping Freedesktop (libnotify) and Gtk (GNotification) + * notifications and forwarding them to supporting devices. + */ +const Listener = GObject.registerClass({ + GTypeName: 'GSConnectNotificationListener', + Signals: { + 'notification-added': { + flags: GObject.SignalFlags.RUN_LAST, + param_types: [GLib.Variant.$gtype], + }, + }, +}, class Listener extends GObject.Object { + + _init() { + super._init(); + + // Respect desktop notification settings + this._settings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.notifications', + }); + + // Watch for new application policies + this._settingsId = this._settings.connect( + 'changed::application-children', + this._onSettingsChanged.bind(this) + ); + + // Cache for appName->desktop-id lookups + this._names = {}; + + // Asynchronous setup + this._init_async(); + } + + get applications() { + if (this._applications === undefined) + this._onSettingsChanged(); + + return this._applications; + } + + /** + * Update application notification settings + */ + _onSettingsChanged() { + this._applications = {}; + + for (const app of this._settings.get_strv('application-children')) { + const appSettings = new Gio.Settings({ + schema_id: 'org.gnome.desktop.notifications.application', + path: `/org/gnome/desktop/notifications/application/${app}/`, + }); + + const appInfo = Gio.DesktopAppInfo.new( + appSettings.get_string('application-id') + ); + + if (appInfo !== null) + this._applications[appInfo.get_name()] = appSettings; + } + } + + async _listNames() { + const reply = await this._session.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'ListNames', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null); + + return reply.deepUnpack()[0]; + } + + async _getNameOwner(name) { + const reply = await this._session.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus', + 'GetNameOwner', + new GLib.Variant('(s)', [name]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + + return reply.deepUnpack()[0]; + } + + /** + * Try and find a well-known name for @sender on the session bus + * + * @param {string} sender - A DBus unique name (eg. :1.2282) + * @param {string} appName - @appName passed to Notify() (Optional) + * @return {string} A well-known name or %null + */ + async _getAppId(sender, appName) { + try { + // Get a list of well-known names, ignoring @sender + const names = await this._listNames(); + names.splice(names.indexOf(sender), 1); + + // Make a short list for substring matches (fractal/org.gnome.Fractal) + const appLower = appName.toLowerCase(); + + const shortList = names.filter(name => { + return name.toLowerCase().includes(appLower); + }); + + // Run the short list first + for (const name of shortList) { + const nameOwner = await this._getNameOwner(name); + + if (nameOwner === sender) + return name; + + names.splice(names.indexOf(name), 1); + } + + // Run the full list + for (const name of names) { + const nameOwner = await this._getNameOwner(name); + + if (nameOwner === sender) + return name; + } + + return null; + } catch (e) { + debug(e); + return null; + } + } + + /** + * Try and find the application name for @sender + * + * @param {string} sender - A DBus unique name + * @param {string} [appName] - `appName` supplied by Notify() + * @return {string} A well-known name or %null + */ + async _getAppName(sender, appName = null) { + // Check the cache first + if (appName && this._names.hasOwnProperty(appName)) + return this._names[appName]; + + try { + const appId = await this._getAppId(sender, appName); + const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`); + this._names[appName] = appInfo.get_name(); + appName = appInfo.get_name(); + } catch (e) { + // Silence errors + } + + return appName; + } + + /** + * Callback for AddNotification()/Notify() + * + * @param {DBus.Interface} iface - The DBus interface + * @param {string} name - The DBus method name + * @param {GLib.Variant} parameters - The method parameters + * @param {Gio.DBusMethodInvocation} invocation - The method invocation info + */ + async _onHandleMethodCall(iface, name, parameters, invocation) { + try { + // Check if notifications are disabled in desktop settings + if (!this._settings.get_boolean('show-banners')) + return; + + parameters = parameters.full_unpack(); + + // GNotification + if (name === 'AddNotification') { + this.AddNotification(...parameters); + + // libnotify + } else if (name === 'Notify') { + const message = invocation.get_message(); + const destination = message.get_destination(); + + // Deduplicate notifications; only accept messages + // directed to the notification bus, or its owner. + if (destination !== 'org.freedesktop.Notifications') { + if (this._fdoNameOwner === undefined) { + this._fdoNameOwner = await this._getNameOwner( + 'org.freedesktop.Notifications'); + } + + if (this._fdoNameOwner !== destination) + return; + } + + // Try to brute-force an application name using DBus + if (!this.applications.hasOwnProperty(parameters[0])) { + const sender = message.get_sender(); + parameters[0] = await this._getAppName(sender, parameters[0]); + } + + this.Notify(...parameters); + } + } catch (e) { + debug(e); + } + } + + /** + * Export interfaces for proxying notifications and become a monitor + * + * @return {Promise} A promise for the operation + */ + _monitorConnection() { + // libnotify Interface + this._fdoNotifications = new GjsPrivate.DBusImplementation({ + g_interface_info: FDO_IFACE, + }); + this._fdoMethodCallId = this._fdoNotifications.connect( + 'handle-method-call', this._onHandleMethodCall.bind(this)); + this._fdoNotifications.export(this._monitor, + '/org/freedesktop/Notifications'); + + this._fdoNameOwnerChangedId = this._session.signal_subscribe( + 'org.freedesktop.DBus', + 'org.freedesktop.DBus', + 'NameOwnerChanged', + '/org/freedesktop/DBus', + 'org.freedesktop.Notifications', + Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE, + this._onFdoNameOwnerChanged.bind(this) + ); + + // GNotification Interface + this._gtkNotifications = new GjsPrivate.DBusImplementation({ + g_interface_info: GTK_IFACE, + }); + this._gtkMethodCallId = this._gtkNotifications.connect( + 'handle-method-call', this._onHandleMethodCall.bind(this)); + this._gtkNotifications.export(this._monitor, '/org/gtk/Notifications'); + + // Become a monitor for Fdo & Gtk notifications + return this._monitor.call( + 'org.freedesktop.DBus', + '/org/freedesktop/DBus', + 'org.freedesktop.DBus.Monitoring', + 'BecomeMonitor', + new GLib.Variant('(asu)', [[FDO_MATCH, GTK_MATCH], 0]), + null, + Gio.DBusCallFlags.NONE, + -1, + null); + } + + async _init_async() { + try { + this._session = Gio.DBus.session; + this._monitor = await DBus.newConnection(); + await this._monitorConnection(); + } catch (e) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + } + + _onFdoNameOwnerChanged(connection, sender, object, iface, signal, parameters) { + this._fdoNameOwner = parameters.deepUnpack()[2]; + } + + _sendNotification(notif) { + // Check if this application is disabled in desktop settings + const appSettings = this.applications[notif.appName]; + + if (appSettings && !appSettings.get_boolean('enable')) + return; + + // Send the notification to each supporting device + // TODO: avoid the overhead of the GAction framework with a signal? + const variant = GLib.Variant.full_pack(notif); + this.emit('notification-added', variant); + } + + Notify(appName, replacesId, iconName, summary, body, actions, hints, timeout) { + // Ignore notifications without an appName + if (!appName) + return; + + this._sendNotification({ + appName: appName, + id: `fdo|null|${replacesId}`, + title: summary, + text: body, + ticker: `${summary}: ${body}`, + isClearable: (replacesId !== 0), + icon: iconName, + }); + } + + AddNotification(application, id, notification) { + // Ignore our own notifications or we'll cause a notification loop + if (application === 'org.gnome.Shell.Extensions.GSConnect') + return; + + const appInfo = Gio.DesktopAppInfo.new(`${application}.desktop`); + + // Try to get an icon for the notification + if (!notification.hasOwnProperty('icon')) + notification.icon = appInfo.get_icon() || undefined; + + this._sendNotification({ + appName: appInfo.get_name(), + id: `gtk|${application}|${id}`, + title: notification.title, + text: notification.body, + ticker: `${notification.title}: ${notification.body}`, + isClearable: true, + icon: notification.icon, + }); + } + + destroy() { + try { + if (this._fdoNotifications) { + this._fdoNotifications.disconnect(this._fdoMethodCallId); + this._fdoNotifications.unexport(); + this._session.signal_unsubscribe(this._fdoNameOwnerChangedId); + } + + if (this._gtkNotifications) { + this._gtkNotifications.disconnect(this._gtkMethodCallId); + this._gtkNotifications.unexport(); + } + + if (this._settings) { + this._settings.disconnect(this._settingsId); + this._settings.run_dispose(); + } + + // TODO: Gio.IOErrorEnum: The connection is closed + // this._monitor.close_sync(null); + + GObject.signal_handlers_destroy(this); + } catch (e) { + debug(e); + } + } +}); + + +/** + * The service class for this component + */ +var Component = Listener; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js new file mode 100644 index 0000000..d3f6657 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/pulseaudio.js @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Tweener = imports.tweener.tweener; + +const GIRepository = imports.gi.GIRepository; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + + +// Add gnome-shell's typelib dir to the search path +const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']); +GIRepository.Repository.prepend_search_path(typelibDir); +GIRepository.Repository.prepend_library_path(typelibDir); + +const Gvc = imports.gi.Gvc; + + +/** + * Extend Gvc.MixerStream with a property for returning a user-visible name + */ +Object.defineProperty(Gvc.MixerStream.prototype, 'display_name', { + get: function () { + try { + if (!this.get_ports().length) + return this.description; + + return `${this.get_port().human_port} (${this.description})`; + } catch (e) { + return this.description; + } + }, +}); + + +/** + * A convenience wrapper for Gvc.MixerStream + */ +class Stream { + constructor(mixer, stream) { + this._mixer = mixer; + this._stream = stream; + + this._max = mixer.get_vol_max_norm(); + } + + get muted() { + return this._stream.is_muted; + } + + set muted(bool) { + this._stream.change_is_muted(bool); + } + + // Volume is a double in the range 0-1 + get volume() { + return Math.floor(100 * this._stream.volume / this._max) / 100; + } + + set volume(num) { + this._stream.volume = Math.floor(num * this._max); + this._stream.push_volume(); + } + + /** + * Gradually raise or lower the stream volume to @value + * + * @param {number} value - A number in the range 0-1 + * @param {number} [duration] - Duration to fade in seconds + */ + fade(value, duration = 1) { + Tweener.removeTweens(this); + + if (this._stream.volume > value) { + this._mixer.fading = true; + + Tweener.addTween(this, { + volume: value, + time: duration, + transition: 'easeOutCubic', + onComplete: () => { + this._mixer.fading = false; + }, + }); + } else if (this._stream.volume < value) { + this._mixer.fading = true; + + Tweener.addTween(this, { + volume: value, + time: duration, + transition: 'easeInCubic', + onComplete: () => { + this._mixer.fading = false; + }, + }); + } + } +} + + +/** + * A subclass of Gvc.MixerControl with convenience functions for controlling the + * default input/output volumes. + * + * The Mixer class uses GNOME Shell's Gvc library to control the system volume + * and offers a few convenience functions. + */ +const Mixer = GObject.registerClass({ + GTypeName: 'GSConnectAudioMixer', +}, class Mixer extends Gvc.MixerControl { + _init(params) { + super._init({name: 'GSConnect'}); + + this._previousVolume = undefined; + this._volumeMuted = false; + this._microphoneMuted = false; + + this.open(); + } + + get fading() { + if (this._fading === undefined) + this._fading = false; + + return this._fading; + } + + set fading(bool) { + if (this.fading === bool) + return; + + this._fading = bool; + + if (this.fading) + this.emit('stream-changed', this._output._stream.id); + } + + get input() { + if (this._input === undefined) + this.vfunc_default_source_changed(); + + return this._input; + } + + get output() { + if (this._output === undefined) + this.vfunc_default_sink_changed(); + + return this._output; + } + + vfunc_default_sink_changed(id) { + try { + const sink = this.get_default_sink(); + this._output = (sink) ? new Stream(this, sink) : null; + } catch (e) { + logError(e); + } + } + + vfunc_default_source_changed(id) { + try { + const source = this.get_default_source(); + this._input = (source) ? new Stream(this, source) : null; + } catch (e) { + logError(e); + } + } + + vfunc_state_changed(new_state) { + try { + if (new_state === Gvc.MixerControlState.READY) { + this.vfunc_default_sink_changed(null); + this.vfunc_default_source_changed(null); + } + } catch (e) { + logError(e); + } + } + + /** + * Store the current output volume then lower it to %15 + * + * @param {number} duration - Duration in seconds to fade + */ + lowerVolume(duration = 1) { + try { + if (this.output.volume > 0.15) { + this._previousVolume = Number(this.output.volume); + this.output.fade(0.15, duration); + } + } catch (e) { + logError(e); + } + } + + /** + * Mute the output volume (speakers) + */ + muteVolume() { + try { + if (this.output.muted) + return; + + this.output.muted = true; + this._volumeMuted = true; + } catch (e) { + logError(e); + } + } + + /** + * Mute the input volume (microphone) + */ + muteMicrophone() { + try { + if (this.input.muted) + return; + + this.input.muted = true; + this._microphoneMuted = true; + } catch (e) { + logError(e); + } + } + + /** + * Restore all mixer levels to their previous state + */ + restore() { + try { + // If we muted the microphone, unmute it before restoring the volume + if (this._microphoneMuted) { + this.input.muted = false; + this._microphoneMuted = false; + } + + // If we muted the volume, unmute it before restoring the volume + if (this._volumeMuted) { + this.output.muted = false; + this._volumeMuted = false; + } + + // If a previous volume is defined, raise it back up to that level + if (this._previousVolume !== undefined) { + this.output.fade(this._previousVolume); + this._previousVolume = undefined; + } + } catch (e) { + logError(e); + } + } + + destroy() { + this.close(); + } +}); + + +/** + * The service class for this component + */ +var Component = Mixer; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/session.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/session.js new file mode 100644 index 0000000..512ace2 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/session.js @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + + +const Session = class { + constructor() { + this._connection = Gio.DBus.system; + this._session = null; + + this._initAsync(); + } + + async _initAsync() { + try { + const reply = await this._connection.call( + 'org.freedesktop.login1', + '/org/freedesktop/login1', + 'org.freedesktop.login1.Manager', + 'ListSessions', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null); + + const sessions = reply.deepUnpack()[0]; + const userName = GLib.get_user_name(); + let sessionPath = '/org/freedesktop/login1/session/auto'; + + // eslint-disable-next-line no-unused-vars + for (const [num, uid, name, seat, objectPath] of sessions) { + if (name === userName) { + sessionPath = objectPath; + break; + } + } + + this._session = new Gio.DBusProxy({ + g_connection: this._connection, + g_name: 'org.freedesktop.login1', + g_object_path: sessionPath, + g_interface_name: 'org.freedesktop.login1.Session', + }); + await this._session.init_async(GLib.PRIORITY_DEFAULT, null); + } catch (e) { + this._session = null; + logError(e); + } + } + + get idle() { + if (this._session === null) + return false; + + return this._session.get_cached_property('IdleHint').unpack(); + } + + get locked() { + if (this._session === null) + return false; + + return this._session.get_cached_property('LockedHint').unpack(); + } + + get active() { + // Active if not idle and not locked + return !(this.idle || this.locked); + } + + destroy() { + this._session = null; + } +}; + + +/** + * The service class for this component + */ +var Component = Session; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/sound.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/sound.js new file mode 100644 index 0000000..4634d50 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/sound.js @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + + +/* + * Used to ensure 'audible-bell' is enabled for fallback + */ +const WM_SETTINGS = new Gio.Settings({ + schema_id: 'org.gnome.desktop.wm.preferences', + path: '/org/gnome/desktop/wm/preferences/', +}); + + +var Player = class Player { + + constructor() { + this._playing = new Set(); + } + + get backend() { + if (this._backend === undefined) { + // Prefer GSound + try { + this._gsound = new imports.gi.GSound.Context(); + this._gsound.init(null); + this._backend = 'gsound'; + + // Try falling back to libcanberra, otherwise just re-run the test + // in case one or the other is installed later + } catch (e) { + if (GLib.find_program_in_path('canberra-gtk-play') !== null) { + this._canberra = new Gio.SubprocessLauncher({ + flags: Gio.SubprocessFlags.NONE, + }); + this._backend = 'libcanberra'; + } else { + return null; + } + } + } + + return this._backend; + } + + _canberraPlaySound(name, cancellable) { + const proc = this._canberra.spawnv(['canberra-gtk-play', '-i', name]); + return proc.wait_check_async(cancellable); + } + + async _canberraLoopSound(name, cancellable) { + while (!cancellable.is_cancelled()) + await this._canberraPlaySound(name, cancellable); + } + + _gsoundPlaySound(name, cancellable) { + return new Promise((resolve, reject) => { + this._gsound.play_full( + {'event.id': name}, + cancellable, + (source, res) => { + try { + resolve(source.play_full_finish(res)); + } catch (e) { + reject(e); + } + } + ); + }); + } + + async _gsoundLoopSound(name, cancellable) { + while (!cancellable.is_cancelled()) + await this._gsoundPlaySound(name, cancellable); + } + + _gdkPlaySound(name, cancellable) { + if (this._display === undefined) + this._display = Gdk.Display.get_default(); + + let count = 0; + + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, () => { + try { + if (count++ < 4 && !cancellable.is_cancelled()) { + this._display.beep(); + return GLib.SOURCE_CONTINUE; + } + + return GLib.SOURCE_REMOVE; + } catch (e) { + logError(e); + return GLib.SOURCE_REMOVE; + } + }); + + return !cancellable.is_cancelled(); + } + + _gdkLoopSound(name, cancellable) { + this._gdkPlaySound(name, cancellable); + GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 1500, + this._gdkPlaySound.bind(this, name, cancellable) + ); + } + + async playSound(name, cancellable) { + try { + if (!(cancellable instanceof Gio.Cancellable)) + cancellable = new Gio.Cancellable(); + + this._playing.add(cancellable); + + switch (this.backend) { + case 'gsound': + await this._gsoundPlaySound(name, cancellable); + break; + + case 'canberra': + await this._canberraPlaySound(name, cancellable); + break; + + default: + await this._gdkPlaySound(name, cancellable); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + this._playing.delete(cancellable); + } + } + + async loopSound(name, cancellable) { + try { + if (!(cancellable instanceof Gio.Cancellable)) + cancellable = new Gio.Cancellable(); + + this._playing.add(cancellable); + + switch (this.backend) { + case 'gsound': + await this._gsoundLoopSound(name, cancellable); + break; + + case 'canberra': + await this._canberraLoopSound(name, cancellable); + break; + + default: + await this._gdkLoopSound(name, cancellable); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + logError(e); + } finally { + this._playing.delete(cancellable); + } + } + + destroy() { + for (const cancellable of this._playing) + cancellable.cancel(); + } +}; + + +/** + * The service class for this component + */ +var Component = Player; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/upower.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/upower.js new file mode 100644 index 0000000..f090a92 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/upower.js @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/** + * The warning level of a battery. + * + * @readonly + * @enum {number} + */ +const DeviceLevel = { + UNKNOWN: 0, + NONE: 1, + DISCHARGING: 2, + LOW: 3, + CRITICAL: 4, + ACTION: 5, + NORMAL: 6, + HIGH: 7, + FULL: 8, + LAST: 9, +}; + +/** + * The device state. + * + * @readonly + * @enum {number} + */ +const DeviceState = { + UNKNOWN: 0, + CHARGING: 1, + DISCHARGING: 2, + EMPTY: 3, + FULLY_CHARGED: 4, + PENDING_CHARGE: 5, + PENDING_DISCHARGE: 6, + LAST: 7, +}; + + +/** + * A class representing the system battery. + */ +var Battery = GObject.registerClass({ + GTypeName: 'GSConnectSystemBattery', + Signals: { + 'changed': { + flags: GObject.SignalFlags.RUN_FIRST, + }, + }, + Properties: { + 'charging': GObject.ParamSpec.boolean( + 'charging', + 'Charging', + 'The current charging state.', + GObject.ParamFlags.READABLE, + false + ), + 'level': GObject.ParamSpec.int( + 'level', + 'Level', + 'The current power level.', + GObject.ParamFlags.READABLE, + -1, 100, + -1 + ), + 'threshold': GObject.ParamSpec.uint( + 'threshold', + 'Threshold', + 'The current threshold state.', + GObject.ParamFlags.READABLE, + 0, 1, + 0 + ), + }, +}, class Battery extends GObject.Object { + + _init() { + super._init(); + + this._cancellable = new Gio.Cancellable(); + this._proxy = null; + this._propertiesChangedId = 0; + + this._loadUPower(); + } + + async _loadUPower() { + try { + this._proxy = new Gio.DBusProxy({ + g_bus_type: Gio.BusType.SYSTEM, + g_name: 'org.freedesktop.UPower', + g_object_path: '/org/freedesktop/UPower/devices/DisplayDevice', + g_interface_name: 'org.freedesktop.UPower.Device', + g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START, + }); + + await this._proxy.init_async(GLib.PRIORITY_DEFAULT, + this._cancellable); + + this._propertiesChangedId = this._proxy.connect( + 'g-properties-changed', this._onPropertiesChanged.bind(this)); + + this._initProperties(this._proxy); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) { + const service = Gio.Application.get_default(); + + if (service !== null) + service.notify_error(e); + else + logError(e); + } + + this._proxy = null; + } + } + + _initProperties(proxy) { + if (proxy.g_name_owner === null) + return; + + const percentage = proxy.get_cached_property('Percentage').unpack(); + const state = proxy.get_cached_property('State').unpack(); + const level = proxy.get_cached_property('WarningLevel').unpack(); + + this._level = Math.floor(percentage); + this._charging = (state !== DeviceState.DISCHARGING); + this._threshold = (!this.charging && level >= DeviceLevel.LOW); + + this.emit('changed'); + } + + _onPropertiesChanged(proxy, changed, invalidated) { + let emitChanged = false; + const properties = changed.deepUnpack(); + + if (properties.hasOwnProperty('Percentage')) { + emitChanged = true; + + const value = proxy.get_cached_property('Percentage').unpack(); + this._level = Math.floor(value); + this.notify('level'); + } + + if (properties.hasOwnProperty('State')) { + emitChanged = true; + + const value = proxy.get_cached_property('State').unpack(); + this._charging = (value !== DeviceState.DISCHARGING); + this.notify('charging'); + } + + if (properties.hasOwnProperty('WarningLevel')) { + emitChanged = true; + + const value = proxy.get_cached_property('WarningLevel').unpack(); + this._threshold = (!this.charging && value >= DeviceLevel.LOW); + this.notify('threshold'); + } + + if (emitChanged) + this.emit('changed'); + } + + get charging() { + if (this._charging === undefined) + this._charging = false; + + return this._charging; + } + + get is_present() { + return (this._proxy && this._proxy.g_name_owner); + } + + get level() { + if (this._level === undefined) + this._level = -1; + + return this._level; + } + + get threshold() { + if (this._threshold === undefined) + this._threshold = 0; + + return this._threshold; + } + + destroy() { + if (this._cancellable.is_cancelled()) + return; + + this._cancellable.cancel(); + + if (this._proxy && this._propertiesChangedId > 0) { + this._proxy.disconnect(this._propertiesChangedId); + this._propertiesChangedId = 0; + } + } +}); + + +/** + * The service class for this component + */ +var Component = Battery; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/ydotool.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/ydotool.js new file mode 100644 index 0000000..c2a7f5d --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/components/ydotool.js @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: JingMatrix https://github.com/JingMatrix +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const Gdk = imports.gi.Gdk; + +const keyCodes = new Map([ + ['1', 2], + ['2', 3], + ['3', 4], + ['4', 5], + ['5', 6], + ['6', 7], + ['7', 8], + ['8', 9], + ['9', 10], + ['0', 11], + ['-', 12], + ['=', 13], + ['Q', 16], + ['W', 17], + ['E', 18], + ['R', 19], + ['T', 20], + ['Y', 21], + ['U', 22], + ['I', 23], + ['O', 24], + ['P', 25], + ['[', 26], + [']', 27], + ['A', 30], + ['S', 31], + ['D', 32], + ['F', 33], + ['G', 34], + ['H', 35], + ['J', 36], + ['K', 37], + ['L', 38], + [';', 39], + ["'", 40], + ['Z', 44], + ['X', 45], + ['C', 46], + ['V', 47], + ['B', 48], + ['N', 49], + ['M', 50], + [',', 51], + ['.', 52], + ['/', 53], + ['\\', 43], +]); +class Controller { + constructor() { + // laucher for wl-clipboard + this._launcher = new Gio.SubprocessLauncher({ + flags: + Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_MERGE, + }); + this._args = []; + this.buttonMap = new Map([ + [Gdk.BUTTON_PRIMARY, '0'], + [Gdk.BUTTON_MIDDLE, '2'], + [Gdk.BUTTON_SECONDARY, '1'], + ]); + } + + get args() { + return this._args; + } + + set args(opts) { + this._args = ['ydotool'].concat(opts); + try { + this._launcher.spawnv(this._args); + } catch (e) { + debug(e, this._args); + } + } + + /* + * Pointer Events + */ + movePointer(dx, dy) { + if (dx === 0 && dy === 0) + return; + this.args = ['mousemove', '--', dx.toString(), dy.toString()]; + } + + pressPointer(button) { + this.args = ['click', '0x4' + this.buttonMap.get(button)]; + } + + releasePointer(button) { + this.args = ['click', '0x8' + this.buttonMap.get(button)]; + } + + clickPointer(button) { + this.args = ['click', '0xC' + this.buttonMap.get(button)]; + } + + doubleclickPointer(button) { + this.args = [ + 'click', + '0xC' + this.buttonMap.get(button), + 'click', + '0xC' + this.buttonMap.get(button), + ]; + } + + scrollPointer(dx, dy) { + if (dx === 0 && dy === 0) + return; + this.args = ['mousemove', '-w', '--', dx.toString(), dy.toString()]; + } + + /* + * Keyboard Events + */ + + pressKeys(input, modifiers_codes) { + if (typeof input === 'string' && modifiers_codes.length === 0) { + try { + this._launcher.spawnv(['wtype', input]); + } catch (e) { + debug(e); + this.arg = ['type', '--', input]; + } + } else { + if (typeof input === 'number') { + modifiers_codes.push(input); + } else if (typeof input === 'string') { + input = input.toUpperCase(); + for (var i = 0; i < input.length; i++) { + if (keyCodes.get(input[i])) { + modifiers_codes.push(keyCodes.get(input[i])); + } else { + debug('Keycode for ' + input[i] + ' not found'); + return; + } + } + + } + this._args = ['key']; + modifiers_codes.forEach((code) => this._args.push(code + ':1')); + modifiers_codes + .reverse() + .forEach((code) => this._args.push(code + ':0')); + this.args = this._args; + } + } + + destroy() { + this._args = []; + } +} + +/** + * The service class for this component + */ +var Component = Controller; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/core.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/core.js new file mode 100644 index 0000000..2bd4fb1 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/core.js @@ -0,0 +1,698 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/** + * Get the local device type. + * + * @return {string} A device type string + */ +function _getDeviceType() { + try { + let type = GLib.file_get_contents('/sys/class/dmi/id/chassis_type')[1]; + + type = Number(new TextDecoder().decode(type)); + + if ([8, 9, 10, 14].includes(type)) + return 'laptop'; + + return 'desktop'; + } catch (e) { + return 'desktop'; + } +} + + +/** + * The packet class is a simple Object-derived class, offering some conveniences + * for working with KDE Connect packets. + */ +var Packet = class Packet { + + constructor(data = null) { + this.id = 0; + this.type = undefined; + this.body = {}; + + if (typeof data === 'string') + Object.assign(this, JSON.parse(data)); + else if (data !== null) + Object.assign(this, data); + } + + [Symbol.toPrimitive](hint) { + this.id = Date.now(); + + if (hint === 'string') + return `${JSON.stringify(this)}\n`; + + if (hint === 'number') + return `${JSON.stringify(this)}\n`.length; + + return true; + } + + get [Symbol.toStringTag]() { + return `Packet:${this.type}`; + } + + /** + * Deserialize and return a new Packet from an Object or string. + * + * @param {Object|string} data - A string or dictionary to deserialize + * @return {Core.Packet} A new packet object + */ + static deserialize(data) { + return new Packet(data); + } + + /** + * Serialize the packet as a single line with a terminating new-line (`\n`) + * character, ready to be written to a channel. + * + * @return {string} A serialized packet + */ + serialize() { + this.id = Date.now(); + return `${JSON.stringify(this)}\n`; + } + + /** + * Update the packet from a dictionary or string of JSON + * + * @param {Object|string} data - Source data + */ + update(data) { + try { + if (typeof data === 'string') + Object.assign(this, JSON.parse(data)); + else + Object.assign(this, data); + } catch (e) { + throw Error(`Malformed data: ${e.message}`); + } + } + + /** + * Check if the packet has a payload. + * + * @return {boolean} %true if @packet has a payload + */ + hasPayload() { + if (!this.hasOwnProperty('payloadSize')) + return false; + + if (!this.hasOwnProperty('payloadTransferInfo')) + return false; + + return (Object.keys(this.payloadTransferInfo).length > 0); + } +}; + + +/** + * Channel objects handle KDE Connect packet exchange and data transfers for + * devices. The implementation is responsible for all negotiation of the + * underlying protocol. + */ +var Channel = GObject.registerClass({ + GTypeName: 'GSConnectChannel', + Properties: { + 'closed': GObject.ParamSpec.boolean( + 'closed', + 'Closed', + 'Whether the channel has been closed', + GObject.ParamFlags.READABLE, + false + ), + }, +}, class Channel extends GObject.Object { + + get address() { + throw new GObject.NotImplementedError(); + } + + get backend() { + if (this._backend === undefined) + this._backend = null; + + return this._backend; + } + + set backend(backend) { + this._backend = backend; + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get closed() { + if (this._closed === undefined) + this._closed = false; + + return this._closed; + } + + get input_stream() { + if (this._input_stream === undefined) { + if (this._connection instanceof Gio.IOStream) + return this._connection.get_input_stream(); + + return null; + } + + return this._input_stream; + } + + set input_stream(stream) { + this._input_stream = stream; + } + + get output_stream() { + if (this._output_stream === undefined) { + if (this._connection instanceof Gio.IOStream) + return this._connection.get_output_stream(); + + return null; + } + + return this._output_stream; + } + + set output_stream(stream) { + this._output_stream = stream; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = GLib.uuid_string_random(); + + return this._uuid; + } + + set uuid(uuid) { + this._uuid = uuid; + } + + /** + * Close the channel. + */ + close() { + throw new GObject.NotImplementedError(); + } + + /** + * Read a packet. + * + * @param {Gio.Cancellable} [cancellable] - A cancellable + * @return {Promise} The packet + */ + async readPacket(cancellable = null) { + if (cancellable === null) + cancellable = this.cancellable; + + if (!(this.input_stream instanceof Gio.DataInputStream)) { + this.input_stream = new Gio.DataInputStream({ + base_stream: this.input_stream, + }); + } + + const [data] = await this.input_stream.read_line_async( + GLib.PRIORITY_DEFAULT, cancellable); + + if (data === null) { + throw new Gio.IOErrorEnum({ + message: 'End of stream', + code: Gio.IOErrorEnum.CONNECTION_CLOSED, + }); + } + + return new Packet(data); + } + + /** + * Send a packet. + * + * @param {Core.Packet} packet - The packet to send + * @param {Gio.Cancellable} [cancellable] - A cancellable + * @return {Promise} %true if successful + */ + sendPacket(packet, cancellable = null) { + if (cancellable === null) + cancellable = this.cancellable; + + return this.output_stream.write_all_async(packet.serialize(), + GLib.PRIORITY_DEFAULT, cancellable); + } + + /** + * Reject a transfer. + * + * @param {Core.Packet} packet - A packet with payload info + */ + rejectTransfer(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Download a payload from a device. Typically implementations will override + * this with an async function. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.OutputStream} target - The target stream + * @param {Gio.Cancellable} [cancellable] - A cancellable for the upload + */ + download(packet, target, cancellable = null) { + throw new GObject.NotImplementedError(); + } + + + /** + * Upload a payload to a device. Typically implementations will override + * this with an async function. + * + * @param {Core.Packet} packet - The packet describing the transfer + * @param {Gio.InputStream} source - The source stream + * @param {number} size - The payload size + * @param {Gio.Cancellable} [cancellable] - A cancellable for the upload + */ + upload(packet, source, size, cancellable = null) { + throw new GObject.NotImplementedError(); + } +}); + + +/** + * ChannelService implementations provide Channel objects, emitting the + * ChannelService::channel signal when a new connection has been accepted. + */ +var ChannelService = GObject.registerClass({ + GTypeName: 'GSConnectChannelService', + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the service is active', + GObject.ParamFlags.READABLE, + false + ), + 'id': GObject.ParamSpec.string( + 'id', + 'ID', + 'The hostname or other network unique id', + GObject.ParamFlags.READWRITE, + null + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The name of the backend', + GObject.ParamFlags.READWRITE, + null + ), + }, + Signals: { + 'channel': { + flags: GObject.SignalFlags.RUN_LAST, + param_types: [Channel.$gtype], + return_type: GObject.TYPE_BOOLEAN, + }, + }, +}, class ChannelService extends GObject.Object { + + get active() { + if (this._active === undefined) + this._active = false; + + return this._active; + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get name() { + if (this._name === undefined) + this._name = GLib.get_host_name(); + + return this._name; + } + + set name(name) { + if (this.name === name) + return; + + this._name = name; + this.notify('name'); + } + + get id() { + if (this._id === undefined) + this._id = GLib.uuid_string_random(); + + return this._id; + } + + set id(id) { + if (this.id === id) + return; + + this._id = id; + } + + get identity() { + if (this._identity === undefined) + this.buildIdentity(); + + return this._identity; + } + + /** + * Broadcast directly to @address or the whole network if %null + * + * @param {string} [address] - A string address + */ + broadcast(address = null) { + throw new GObject.NotImplementedError(); + } + + /** + * Rebuild the identity packet used to identify the local device. An + * implementation may override this to make modifications to the default + * capabilities if necessary (eg. bluez without SFTP support). + */ + buildIdentity() { + this._identity = new Packet({ + id: 0, + type: 'kdeconnect.identity', + body: { + deviceId: this.id, + deviceName: this.name, + deviceType: _getDeviceType(), + protocolVersion: 7, + incomingCapabilities: [], + outgoingCapabilities: [], + }, + }); + + for (const name in imports.service.plugins) { + // Exclude mousepad/presenter capability in unsupported sessions + if (!HAVE_REMOTEINPUT && ['mousepad', 'presenter'].includes(name)) + continue; + + const meta = imports.service.plugins[name].Metadata; + + if (meta === undefined) + continue; + + for (const type of meta.incomingCapabilities) + this._identity.body.incomingCapabilities.push(type); + + for (const type of meta.outgoingCapabilities) + this._identity.body.outgoingCapabilities.push(type); + } + } + + /** + * Emit Core.ChannelService::channel + * + * @param {Core.Channel} channel - The new channel + */ + channel(channel) { + if (!this.emit('channel', channel)) + channel.close(); + } + + /** + * Start the channel service. Implementations should throw an error if the + * service fails to meet any of its requirements for opening or accepting + * connections. + */ + start() { + throw new GObject.NotImplementedError(); + } + + /** + * Stop the channel service. + */ + stop() { + throw new GObject.NotImplementedError(); + } + + /** + * Destroy the channel service. + */ + destroy() { + } +}); + + +/** + * A class representing a file transfer. + */ +var Transfer = GObject.registerClass({ + GTypeName: 'GSConnectTransfer', + Properties: { + 'channel': GObject.ParamSpec.object( + 'channel', + 'Channel', + 'The channel that owns this transfer', + GObject.ParamFlags.READWRITE, + Channel.$gtype + ), + 'completed': GObject.ParamSpec.boolean( + 'completed', + 'Completed', + 'Whether the transfer has completed', + GObject.ParamFlags.READABLE, + false + ), + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device that created this transfer', + GObject.ParamFlags.READWRITE, + GObject.Object.$gtype + ), + }, +}, class Transfer extends GObject.Object { + + _init(params = {}) { + super._init(params); + + this._cancellable = new Gio.Cancellable(); + this._items = []; + } + + get channel() { + if (this._channel === undefined) + this._channel = null; + + return this._channel; + } + + set channel(channel) { + if (this.channel === channel) + return; + + this._channel = channel; + } + + get completed() { + if (this._completed === undefined) + this._completed = false; + + return this._completed; + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + if (this.device === device) + return; + + this._device = device; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = GLib.uuid_string_random(); + + return this._uuid; + } + + /** + * Ensure there is a stream for the transfer item. + * + * @param {Object} item - A transfer item + * @param {Gio.Cancellable} [cancellable] - A cancellable + */ + async _ensureStream(item, cancellable = null) { + // This is an upload from a remote device + if (item.packet.hasPayload()) { + if (item.target instanceof Gio.OutputStream) + return; + + if (item.file instanceof Gio.File) { + item.target = await item.file.replace_async( + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + GLib.PRIORITY_DEFAULT, + this._cancellable); + } + } else { + if (item.source instanceof Gio.InputStream) + return; + + if (item.file instanceof Gio.File) { + const read = item.file.read_async(GLib.PRIORITY_DEFAULT, + cancellable); + + const query = item.file.query_info_async( + Gio.FILE_ATTRIBUTE_STANDARD_SIZE, + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + cancellable); + + const [stream, info] = await Promise.all([read, query]); + item.source = stream; + item.size = info.get_size(); + } + } + } + + /** + * Add a file to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.File} file - A file to transfer + */ + addFile(packet, file) { + const item = { + packet: new Packet(packet), + file: file, + source: null, + target: null, + }; + + this._items.push(item); + } + + /** + * Add a filepath to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {string} path - A filepath to transfer + */ + addPath(packet, path) { + const item = { + packet: new Packet(packet), + file: Gio.File.new_for_path(path), + source: null, + target: null, + }; + + this._items.push(item); + } + + /** + * Add a stream to the transfer. + * + * @param {Core.Packet} packet - A packet + * @param {Gio.InputStream|Gio.OutputStream} stream - A stream to transfer + * @param {number} [size] - Payload size + */ + addStream(packet, stream, size = 0) { + const item = { + packet: new Packet(packet), + file: null, + source: null, + target: null, + size: size, + }; + + if (stream instanceof Gio.InputStream) + item.source = stream; + else if (stream instanceof Gio.OutputStream) + item.target = stream; + + this._items.push(item); + } + + /** + * Execute a transfer operation. Implementations may override this, while + * the default uses g_output_stream_splice(). + * + * @param {Gio.Cancellable} [cancellable] - A cancellable + */ + async start(cancellable = null) { + let error = null; + + try { + let item; + + // If a cancellable is passed in, chain to its signal + if (cancellable instanceof Gio.Cancellable) + cancellable.connect(() => this._cancellable.cancel()); + + while ((item = this._items.shift())) { + // If created for a device, ignore connection changes by + // ensuring we have the most recent channel + if (this.device !== null) + this._channel = this.device.channel; + + // TODO: transfer queueing? + if (this.channel === null || this.channel.closed) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.CONNECTION_CLOSED, + message: 'Channel is closed', + }); + } + + await this._ensureStream(item, this._cancellable); + + if (item.packet.hasPayload()) { + await this.channel.download(item.packet, item.target, + this._cancellable); + } else { + await this.channel.upload(item.packet, item.source, + item.size, this._cancellable); + } + } + } catch (e) { + error = e; + } finally { + this._completed = true; + this.notify('completed'); + } + + if (error !== null) + throw error; + } + + cancel() { + if (this._cancellable.is_cancelled() === false) + this._cancellable.cancel(); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/daemon.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/daemon.js new file mode 100755 index 0000000..0d51748 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/daemon.js @@ -0,0 +1,721 @@ +#!/usr/bin/env gjs + +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +imports.gi.versions.Gdk = '3.0'; +imports.gi.versions.GdkPixbuf = '2.0'; +imports.gi.versions.Gio = '2.0'; +imports.gi.versions.GIRepository = '2.0'; +imports.gi.versions.GLib = '2.0'; +imports.gi.versions.GObject = '2.0'; +imports.gi.versions.Gtk = '3.0'; +imports.gi.versions.Pango = '1.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 [, path] = /@([^:]+):\d+/.exec(new Error().stack.split('\n')[1]); + const info = Gio.File.new_for_path(path) + .query_info('standard::*', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); + path = info.get_is_symlink() ? info.get_symlink_target() : path; + + return Gio.File.new_for_path(path).get_parent().get_parent().get_path(); +} + +imports.searchPath.unshift(get_datadir()); +imports.config.PACKAGE_DATADIR = imports.searchPath[0]; + +const _setup = imports.service.utils.setup; + + +// Local Imports +const Config = imports.config; +const Manager = imports.service.manager; +const ServiceUI = imports.service.ui.service; + + +/** + * Class representing the GSConnect service daemon. + */ +const Service = GObject.registerClass({ + GTypeName: 'GSConnectService', +}, class Service extends Gtk.Application { + + _init() { + super._init({ + application_id: 'org.gnome.Shell.Extensions.GSConnect', + flags: Gio.ApplicationFlags.HANDLES_OPEN, + resource_base_path: '/org/gnome/Shell/Extensions/GSConnect', + }); + + GLib.set_prgname('gsconnect'); + GLib.set_application_name('GSConnect'); + + // Command-line + this._initOptions(); + } + + get settings() { + if (this._settings === undefined) { + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + } + + return this._settings; + } + + /* + * GActions + */ + _initActions() { + const actions = [ + ['connect', this._identify.bind(this), new GLib.VariantType('s')], + ['device', this._device.bind(this), new GLib.VariantType('(ssbv)')], + ['error', this._error.bind(this), new GLib.VariantType('a{ss}')], + ['preferences', this._preferences, null], + ['quit', () => this.quit(), null], + ['refresh', this._identify.bind(this), null], + ]; + + for (const [name, callback, type] of actions) { + const action = new Gio.SimpleAction({ + name: name, + parameter_type: type, + }); + action.connect('activate', callback); + this.add_action(action); + } + } + + /** + * A wrapper for Device GActions. This is used to route device notification + * actions to their device, since GNotifications need an 'app' level action. + * + * @param {Gio.Action} action - The GAction + * @param {GLib.Variant} parameter - The activation parameter + */ + _device(action, parameter) { + try { + parameter = parameter.unpack(); + + // Select the appropriate device(s) + let devices; + const id = parameter[0].unpack(); + + if (id === '*') + devices = this.manager.devices.values(); + else + devices = [this.manager.devices.get(id)]; + + // Unpack the action data and activate the action + const name = parameter[1].unpack(); + const target = parameter[2].unpack() ? parameter[3].unpack() : null; + + for (const device of devices) + device.activate_action(name, target); + } catch (e) { + logError(e); + } + } + + _error(action, parameter) { + try { + const error = parameter.deepUnpack(); + + // If there's a URL, we have better information in the Wiki + if (error.url !== undefined) { + Gio.AppInfo.launch_default_for_uri_async( + error.url, + null, + null, + null + ); + return; + } + + const dialog = new ServiceUI.ErrorDialog(error); + dialog.present(); + } catch (e) { + logError(e); + } + } + + _identify(action, parameter) { + try { + let uri = null; + + if (parameter instanceof GLib.Variant) + uri = parameter.unpack(); + + this.manager.identify(uri); + } catch (e) { + logError(e); + } + } + + _preferences() { + Gio.Subprocess.new( + [`${Config.PACKAGE_DATADIR}/gsconnect-preferences`], + Gio.SubprocessFlags.NONE + ); + } + + /** + * Report a service-level error + * + * @param {Object} error - An Error or object with name, message and stack + */ + notify_error(error) { + try { + // Always log the error + logError(error); + + // Create an new notification + let id, body, priority; + const notif = new Gio.Notification(); + const icon = new Gio.ThemedIcon({name: 'dialog-error'}); + let target = null; + + if (error.name === undefined) + error.name = 'Error'; + + if (error.url !== undefined) { + id = error.url; + body = _('Click for help troubleshooting'); + priority = Gio.NotificationPriority.URGENT; + + target = new GLib.Variant('a{ss}', { + name: error.name.trim(), + message: error.message.trim(), + stack: error.stack.trim(), + url: error.url, + }); + } else { + id = error.message.trim(); + body = _('Click for more information'); + priority = Gio.NotificationPriority.HIGH; + + target = new GLib.Variant('a{ss}', { + name: error.name.trim(), + message: error.message.trim(), + stack: error.stack.trim(), + }); + } + + notif.set_title(`GSConnect: ${error.name.trim()}`); + notif.set_body(body); + notif.set_icon(icon); + notif.set_priority(priority); + notif.set_default_action_and_target('app.error', target); + + this.send_notification(id, notif); + } catch (e) { + logError(e); + } + } + + vfunc_activate() { + super.vfunc_activate(); + } + + vfunc_startup() { + super.vfunc_startup(); + + this.hold(); + + // Watch *this* file and stop the service if it's updated/uninstalled + this._serviceMonitor = Gio.File.new_for_path( + `${Config.PACKAGE_DATADIR}/service/daemon.js` + ).monitor(Gio.FileMonitorFlags.WATCH_MOVES, null); + this._serviceMonitor.connect('changed', () => this.quit()); + + // Init some resources + const 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 + ); + + // Ensure our handlers are registered + try { + const appInfo = Gio.DesktopAppInfo.new(`${Config.APP_ID}.desktop`); + appInfo.add_supports_type('x-scheme-handler/sms'); + appInfo.add_supports_type('x-scheme-handler/tel'); + } catch (e) { + debug(e); + } + + // GActions & GSettings + this._initActions(); + + this.manager.start(); + } + + vfunc_dbus_register(connection, object_path) { + if (!super.vfunc_dbus_register(connection, object_path)) + return false; + + this.manager = new Manager.Manager({ + connection: connection, + object_path: object_path, + }); + + return true; + } + + vfunc_dbus_unregister(connection, object_path) { + this.manager.destroy(); + + super.vfunc_dbus_unregister(connection, object_path); + } + + vfunc_open(files, hint) { + super.vfunc_open(files, hint); + + for (const file of files) { + let action, parameter, title; + + try { + switch (file.get_uri_scheme()) { + case 'sms': + title = _('Send SMS'); + action = 'uriSms'; + parameter = new GLib.Variant('s', file.get_uri()); + break; + + case 'tel': + title = _('Dial Number'); + action = 'shareUri'; + parameter = new GLib.Variant('s', file.get_uri()); + break; + + case 'file': + title = _('Share File'); + action = 'shareFile'; + parameter = new GLib.Variant('(sb)', [file.get_uri(), false]); + break; + + default: + throw new Error(`Unsupported URI: ${file.get_uri()}`); + } + + // Show chooser dialog + new ServiceUI.DeviceChooser({ + title: title, + action_name: action, + action_target: parameter, + }); + } catch (e) { + logError(e, `GSConnect: Opening ${file.get_uri()}`); + } + } + } + + vfunc_shutdown() { + // Dispose GSettings + if (this._settings !== undefined) + this.settings.run_dispose(); + + this.manager.stop(); + + // Exhaust the event loop to ensure any pending operations complete + const context = GLib.MainContext.default(); + + while (context.iteration(false)) + continue; + + // Force a GC to prevent any more calls back into JS, then chain-up + imports.system.gc(); + super.vfunc_shutdown(); + } + + /* + * CLI + */ + _initOptions() { + /* + * Device Listings + */ + this.add_main_option( + 'list-devices', + 'l'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('List available devices'), + null + ); + + this.add_main_option( + 'list-all', + 'a'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('List all devices'), + null + ); + + this.add_main_option( + 'device', + 'd'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Target Device'), + '' + ); + + /** + * Pairing + */ + this.add_main_option( + 'pair', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Pair'), + null + ); + + this.add_main_option( + 'unpair', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Unpair'), + null + ); + + /* + * Messaging + */ + this.add_main_option( + 'message', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING_ARRAY, + _('Send SMS'), + '' + ); + + this.add_main_option( + 'message-body', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Message Body'), + '' + ); + + /* + * Notifications + */ + this.add_main_option( + 'notification', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Send Notification'), + '' + ); + + this.add_main_option( + 'notification-appname', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification App Name'), + '<name>' + ); + + this.add_main_option( + 'notification-body', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification Body'), + '<text>' + ); + + this.add_main_option( + 'notification-icon', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification Icon'), + '<icon-name>' + ); + + this.add_main_option( + 'notification-id', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Notification ID'), + '<id>' + ); + + this.add_main_option( + 'ping', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Ping'), + null + ); + + this.add_main_option( + 'ring', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Ring'), + null + ); + + /* + * Sharing + */ + this.add_main_option( + 'share-file', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.FILENAME_ARRAY, + _('Share File'), + '<filepath|URI>' + ); + + this.add_main_option( + 'share-link', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING_ARRAY, + _('Share Link'), + '<URL>' + ); + + this.add_main_option( + 'share-text', + 0, + GLib.OptionFlags.NONE, + GLib.OptionArg.STRING, + _('Share Text'), + '<text>' + ); + + /* + * Misc + */ + this.add_main_option( + 'version', + 'v'.charCodeAt(0), + GLib.OptionFlags.NONE, + GLib.OptionArg.NONE, + _('Show release version'), + null + ); + } + + _cliAction(id, name, parameter = null) { + const parameters = []; + + if (parameter instanceof GLib.Variant) + parameters[0] = parameter; + + id = id.replace(/\W+/g, '_'); + + Gio.DBus.session.call_sync( + 'org.gnome.Shell.Extensions.GSConnect', + `/org/gnome/Shell/Extensions/GSConnect/Device/${id}`, + 'org.gtk.Actions', + 'Activate', + GLib.Variant.new('(sava{sv})', [name, parameters, {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null + ); + } + + _cliListDevices(full = true) { + const result = Gio.DBus.session.call_sync( + 'org.gnome.Shell.Extensions.GSConnect', + '/org/gnome/Shell/Extensions/GSConnect', + 'org.freedesktop.DBus.ObjectManager', + 'GetManagedObjects', + null, + null, + Gio.DBusCallFlags.NONE, + -1, + null + ); + + const variant = result.unpack()[0].unpack(); + let device; + + for (let object of Object.values(variant)) { + object = object.recursiveUnpack(); + device = object['org.gnome.Shell.Extensions.GSConnect.Device']; + + if (full) + print(`${device.Id}\t${device.Name}\t${device.Connected}\t${device.Paired}`); + else if (device.Connected && device.Paired) + print(device.Id); + } + } + + _cliMessage(id, options) { + if (!options.contains('message-body')) + throw new TypeError('missing --message-body option'); + + // TODO: currently we only support single-recipient messaging + const addresses = options.lookup_value('message', null).deepUnpack(); + const body = options.lookup_value('message-body', null).deepUnpack(); + + this._cliAction( + id, + 'sendSms', + GLib.Variant.new('(ss)', [addresses[0], body]) + ); + } + + _cliNotify(id, options) { + const title = options.lookup_value('notification', null).unpack(); + let body = ''; + let icon = null; + let nid = `${Date.now()}`; + let appName = 'GSConnect CLI'; + + if (options.contains('notification-id')) + nid = options.lookup_value('notification-id', null).unpack(); + + if (options.contains('notification-body')) + body = options.lookup_value('notification-body', null).unpack(); + + if (options.contains('notification-appname')) + appName = options.lookup_value('notification-appname', null).unpack(); + + if (options.contains('notification-icon')) { + icon = options.lookup_value('notification-icon', null).unpack(); + icon = Gio.Icon.new_for_string(icon); + } else { + icon = new Gio.ThemedIcon({ + name: 'org.gnome.Shell.Extensions.GSConnect', + }); + } + + const notification = new GLib.Variant('a{sv}', { + appName: GLib.Variant.new_string(appName), + id: GLib.Variant.new_string(nid), + title: GLib.Variant.new_string(title), + text: GLib.Variant.new_string(body), + ticker: GLib.Variant.new_string(`${title}: ${body}`), + time: GLib.Variant.new_string(`${Date.now()}`), + isClearable: GLib.Variant.new_boolean(true), + icon: icon.serialize(), + }); + + this._cliAction(id, 'sendNotification', notification); + } + + _cliShareFile(device, options) { + const files = options.lookup_value('share-file', null).deepUnpack(); + + for (let file of files) { + file = new TextDecoder().decode(file); + this._cliAction(device, 'shareFile', GLib.Variant.new('(sb)', [file, false])); + } + } + + _cliShareLink(device, options) { + const uris = options.lookup_value('share-link', null).unpack(); + + for (const uri of uris) + this._cliAction(device, 'shareUri', uri); + } + + _cliShareText(device, options) { + const text = options.lookup_value('share-text', null).unpack(); + + this._cliAction(device, 'shareText', GLib.Variant.new_string(text)); + } + + vfunc_handle_local_options(options) { + try { + if (options.contains('version')) { + print(`GSConnect ${Config.PACKAGE_VERSION}`); + return 0; + } + + this.register(null); + + if (options.contains('list-devices')) { + this._cliListDevices(false); + return 0; + } + + if (options.contains('list-all')) { + this._cliListDevices(true); + return 0; + } + + // We need a device for anything else; exit since this is probably + // the daemon being started. + if (!options.contains('device')) + return -1; + + const id = options.lookup_value('device', null).unpack(); + + // Pairing + if (options.contains('pair')) { + this._cliAction(id, 'pair'); + return 0; + } + + if (options.contains('unpair')) { + this._cliAction(id, 'unpair'); + return 0; + } + + // Plugins + if (options.contains('message')) + this._cliMessage(id, options); + + if (options.contains('notification')) + this._cliNotify(id, options); + + if (options.contains('ping')) + this._cliAction(id, 'ping', GLib.Variant.new_string('')); + + if (options.contains('ring')) + this._cliAction(id, 'ring'); + + if (options.contains('share-file')) + this._cliShareFile(id, options); + + if (options.contains('share-link')) + this._cliShareLink(id, options); + + if (options.contains('share-text')) + this._cliShareText(id, options); + + return 0; + } catch (e) { + logError(e); + return 1; + } + } +}); + +(new Service()).run([imports.system.programInvocationName].concat(ARGV)); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/device.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/device.js new file mode 100644 index 0000000..3402b6d --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/device.js @@ -0,0 +1,1110 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Components = imports.service.components; +const Core = imports.service.core; + + +/** + * An object representing a remote device. + * + * Device class is subclassed from Gio.SimpleActionGroup so it implements the + * GActionGroup and GActionMap interfaces, like Gio.Application. + * + */ +var Device = GObject.registerClass({ + GTypeName: 'GSConnectDevice', + Properties: { + 'connected': GObject.ParamSpec.boolean( + 'connected', + 'Connected', + 'Whether the device is connected', + GObject.ParamFlags.READABLE, + false + ), + 'contacts': GObject.ParamSpec.object( + 'contacts', + 'Contacts', + 'The contacts store for this device', + GObject.ParamFlags.READABLE, + GObject.Object + ), + 'encryption-info': GObject.ParamSpec.string( + 'encryption-info', + 'Encryption Info', + 'A formatted string with the local and remote fingerprints', + GObject.ParamFlags.READABLE, + null + ), + 'icon-name': GObject.ParamSpec.string( + 'icon-name', + 'Icon Name', + 'Icon name representing the device', + GObject.ParamFlags.READABLE, + null + ), + 'id': GObject.ParamSpec.string( + 'id', + 'Id', + 'The device hostname or other network unique id', + GObject.ParamFlags.READABLE, + '' + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + 'paired': GObject.ParamSpec.boolean( + 'paired', + 'Paired', + 'Whether the device is paired', + GObject.ParamFlags.READABLE, + false + ), + 'type': GObject.ParamSpec.string( + 'type', + 'Type', + 'The device type', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Device extends Gio.SimpleActionGroup { + + _init(identity) { + super._init(); + + this._id = identity.body.deviceId; + + // GLib.Source timeout id's for pairing requests + this._incomingPairRequest = 0; + this._outgoingPairRequest = 0; + + // Maps of name->Plugin, packet->Plugin, uuid->Transfer + this._plugins = new Map(); + this._handlers = new Map(); + this._procs = new Set(); + this._transfers = new Map(); + + this._outputLock = false; + this._outputQueue = []; + + // GSettings + this.settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.Device', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/device/${this.id}/`, + }); + this._migratePlugins(); + + // Watch for changes to supported and disabled plugins + this._disabledPluginsChangedId = this.settings.connect( + 'changed::disabled-plugins', + this._onAllowedPluginsChanged.bind(this) + ); + this._supportedPluginsChangedId = this.settings.connect( + 'changed::supported-plugins', + this._onAllowedPluginsChanged.bind(this) + ); + + this._registerActions(); + this.menu = new Gio.Menu(); + + // Parse identity if initialized with a proper packet, otherwise load + if (identity.id !== undefined) + this._handleIdentity(identity); + else + this._loadPlugins(); + } + + get channel() { + if (this._channel === undefined) + this._channel = null; + + return this._channel; + } + + get connected() { + if (this._connected === undefined) + this._connected = false; + + return this._connected; + } + + get connection_type() { + const lastConnection = this.settings.get_string('last-connection'); + + return lastConnection.split('://')[0]; + } + + get contacts() { + const contacts = this._plugins.get('contacts'); + + if (contacts && contacts.settings.get_boolean('contacts-source')) + return contacts._store; + + if (this._contacts === undefined) + this._contacts = Components.acquire('contacts'); + + return this._contacts; + } + + // FIXME: backend should do this stuff + get encryption_info() { + let localCert = null; + let remoteCert = null; + + // Bluetooth connections have no certificate so we use the host address + if (this.connection_type === 'bluetooth') { + // TRANSLATORS: Bluetooth address for remote device + return _('Bluetooth device at %s').format('???'); + + // If the device is connected use the certificate from the connection + } else if (this.connected) { + remoteCert = this.channel.peer_certificate; + + // Otherwise pull it out of the settings + } else if (this.paired) { + remoteCert = Gio.TlsCertificate.new_from_pem( + this.settings.get_string('certificate-pem'), + -1 + ); + } + + // FIXME: another ugly reach-around + let lanBackend; + + if (this.service !== null) + lanBackend = this.service.manager.backends.get('lan'); + + if (lanBackend && lanBackend.certificate) + localCert = lanBackend.certificate; + + + let verificationKey = ''; + if (localCert && remoteCert) { + let a = localCert.pubkey_der(); + let b = remoteCert.pubkey_der(); + if (a.compare(b) < 0) + [a, b] = [b, a]; // swap + const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256); + checksum.update(a.toArray()); + checksum.update(b.toArray()); + verificationKey = checksum.get_string(); + } + + // TRANSLATORS: Label for TLS connection verification key + // + // Example: + // + // Verification key: 0123456789abcdef000000000000000000000000 + return _('Verification key: %s').format(verificationKey); + } + + get id() { + return this._id; + } + + get name() { + return this.settings.get_string('name'); + } + + get paired() { + return this.settings.get_boolean('paired'); + } + + get icon_name() { + switch (this.type) { + case 'laptop': + return 'laptop-symbolic'; + case 'phone': + return 'smartphone-symbolic'; + case 'tablet': + return 'tablet-symbolic'; + case 'tv': + return 'tv-symbolic'; + case 'desktop': + default: + return 'computer-symbolic'; + } + } + + get service() { + if (this._service === undefined) + this._service = Gio.Application.get_default(); + + return this._service; + } + + get type() { + return this.settings.get_string('type'); + } + + _migratePlugins() { + const deprecated = ['photo']; + const supported = this.settings + .get_strv('supported-plugins') + .filter(name => !deprecated.includes(name)); + + this.settings.set_strv('supported-plugins', supported); + } + + _handleIdentity(packet) { + this.freeze_notify(); + + // If we're connected, record the reconnect URI + if (this.channel !== null) + this.settings.set_string('last-connection', this.channel.address); + + // The type won't change, but it might not be properly set yet + if (this.type !== packet.body.deviceType) { + this.settings.set_string('type', packet.body.deviceType); + this.notify('type'); + this.notify('icon-name'); + } + + // The name may change so we check and notify if so + if (this.name !== packet.body.deviceName) { + this.settings.set_string('name', packet.body.deviceName); + this.notify('name'); + } + + // Packets + const incoming = packet.body.incomingCapabilities.sort(); + const outgoing = packet.body.outgoingCapabilities.sort(); + const inc = this.settings.get_strv('incoming-capabilities'); + const out = this.settings.get_strv('outgoing-capabilities'); + + // Only write GSettings if something has changed + if (incoming.join('') !== inc.join('') || outgoing.join('') !== out.join('')) { + this.settings.set_strv('incoming-capabilities', incoming); + this.settings.set_strv('outgoing-capabilities', outgoing); + } + + // Determine supported plugins by matching incoming to outgoing types + const supported = []; + + for (const name in imports.service.plugins) { + // Exclude mousepad/presenter plugins in unsupported sessions + if (!HAVE_REMOTEINPUT && ['mousepad', 'presenter'].includes(name)) + continue; + + const meta = imports.service.plugins[name].Metadata; + + if (meta === undefined) + continue; + + // If we can handle packets it sends or send packets it can handle + if (meta.incomingCapabilities.some(t => outgoing.includes(t)) || + meta.outgoingCapabilities.some(t => incoming.includes(t))) + supported.push(name); + } + + // Only write GSettings if something has changed + const currentSupported = this.settings.get_strv('supported-plugins'); + + if (currentSupported.join('') !== supported.sort().join('')) + this.settings.set_strv('supported-plugins', supported); + + this.thaw_notify(); + } + + /** + * Set the channel and start sending/receiving packets. If %null is passed + * the device becomes disconnected. + * + * @param {Core.Channel} [channel] - The new channel + */ + setChannel(channel = null) { + if (this.channel === channel) + return; + + if (this.channel !== null) + this.channel.close(); + + this._channel = channel; + + // If we've disconnected empty the queue, otherwise restart the read + // loop and update the device metadata + if (this.channel === null) { + this._outputQueue.length = 0; + } else { + this._handleIdentity(this.channel.identity); + this._readLoop(channel); + } + + // The connected state didn't change + if (this.connected === !!this.channel) + return; + + // Notify and trigger plugins + this._connected = !!this.channel; + this.notify('connected'); + this._triggerPlugins(); + } + + async _readLoop(channel) { + try { + let packet = null; + + while ((packet = await this.channel.readPacket())) { + debug(packet, this.name); + this.handlePacket(packet); + } + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.name); + + if (this.channel === channel) + this.setChannel(null); + } + } + + _processExit(proc, result) { + try { + proc.wait_check_finish(result); + } catch (e) { + debug(e); + } + + this.delete(proc); + } + + /** + * Launch a subprocess for the device. If the device becomes unpaired, it is + * assumed the device is no longer trusted and all subprocesses will be + * killed. + * + * @param {string[]} args - process arguments + * @param {Gio.Cancellable} [cancellable] - optional cancellable + * @return {Gio.Subprocess} The subprocess + */ + launchProcess(args, cancellable = null) { + if (this._launcher === undefined) { + const application = GLib.build_filenamev([ + Config.PACKAGE_DATADIR, + 'service', + 'daemon.js', + ]); + + this._launcher = new Gio.SubprocessLauncher(); + this._launcher.setenv('GSCONNECT', application, false); + this._launcher.setenv('GSCONNECT_DEVICE_ID', this.id, false); + this._launcher.setenv('GSCONNECT_DEVICE_NAME', this.name, false); + this._launcher.setenv('GSCONNECT_DEVICE_ICON', this.icon_name, false); + this._launcher.setenv( + 'GSCONNECT_DEVICE_DBUS', + `${Config.APP_PATH}/Device/${this.id.replace(/\W+/g, '_')}`, + false + ); + } + + // Create and track the process + const proc = this._launcher.spawnv(args); + proc.wait_check_async(cancellable, this._processExit.bind(this._procs)); + this._procs.add(proc); + + return proc; + } + + /** + * Handle a packet and pass it to the appropriate plugin. + * + * @param {Core.Packet} packet - The incoming packet object + * @return {undefined} no return value + */ + handlePacket(packet) { + try { + if (packet.type === 'kdeconnect.pair') + return this._handlePair(packet); + + // The device must think we're paired; inform it we are not + if (!this.paired) + return this.unpair(); + + const handler = this._handlers.get(packet.type); + + if (handler !== undefined) + handler.handlePacket(packet); + else + debug(`Unsupported packet type (${packet.type})`, this.name); + } catch (e) { + debug(e, this.name); + } + } + + /** + * Send a packet to the device. + * + * @param {Object} packet - An object of packet data... + */ + async sendPacket(packet) { + try { + if (!this.connected) + return; + + if (!this.paired && packet.type !== 'kdeconnect.pair') + return; + + this._outputQueue.push(new Core.Packet(packet)); + + if (this._outputLock) + return; + + this._outputLock = true; + let next; + + while ((next = this._outputQueue.shift())) { + await this.channel.sendPacket(next); + debug(next, this.name); + } + + this._outputLock = false; + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.name); + + this._outputLock = false; + } + } + + /** + * Actions + */ + _registerActions() { + // Pairing notification actions + const acceptPair = new Gio.SimpleAction({name: 'pair'}); + acceptPair.connect('activate', this.pair.bind(this)); + this.add_action(acceptPair); + + const rejectPair = new Gio.SimpleAction({name: 'unpair'}); + rejectPair.connect('activate', this.unpair.bind(this)); + this.add_action(rejectPair); + + // Transfer notification actions + const cancelTransfer = new Gio.SimpleAction({ + name: 'cancelTransfer', + parameter_type: new GLib.VariantType('s'), + }); + cancelTransfer.connect('activate', this.cancelTransfer.bind(this)); + this.add_action(cancelTransfer); + + const openPath = new Gio.SimpleAction({ + name: 'openPath', + parameter_type: new GLib.VariantType('s'), + }); + openPath.connect('activate', this.openPath); + this.add_action(openPath); + + const showPathInFolder = new Gio.SimpleAction({ + name: 'showPathInFolder', + parameter_type: new GLib.VariantType('s'), + }); + showPathInFolder.connect('activate', this.showPathInFolder); + this.add_action(showPathInFolder); + + // Preference helpers + const clearCache = new Gio.SimpleAction({ + name: 'clearCache', + parameter_type: null, + }); + clearCache.connect('activate', this._clearCache.bind(this)); + this.add_action(clearCache); + } + + /** + * Get the position of a GMenuItem with @actionName in the top level of the + * device menu. + * + * @param {string} actionName - An action name with scope (eg. device.foo) + * @return {number} An 0-based index or -1 if not found + */ + getMenuAction(actionName) { + for (let i = 0, len = this.menu.get_n_items(); i < len; i++) { + try { + const val = this.menu.get_item_attribute_value(i, 'action', null); + + if (val.unpack() === actionName) + return i; + } catch (e) { + continue; + } + } + + return -1; + } + + /** + * Add a GMenuItem to the top level of the device menu + * + * @param {Gio.MenuItem} menuItem - A GMenuItem + * @param {number} [index] - The position to place the item + * @return {number} The position the item was placed + */ + addMenuItem(menuItem, index = -1) { + try { + if (index > -1) { + this.menu.insert_item(index, menuItem); + return index; + } + + this.menu.append_item(menuItem); + return this.menu.get_n_items(); + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Add a Device GAction to the top level of the device menu + * + * @param {Gio.Action} action - A GAction + * @param {number} [index] - The position to place the item + * @param {string} label - A label for the item + * @param {string} icon_name - A themed icon name for the item + * @return {number} The position the item was placed + */ + addMenuAction(action, index = -1, label, icon_name) { + try { + const item = new Gio.MenuItem(); + + if (label) + item.set_label(label); + + if (icon_name) + item.set_icon(new Gio.ThemedIcon({name: icon_name})); + + item.set_attribute_value( + 'hidden-when', + new GLib.Variant('s', 'action-disabled') + ); + + item.set_detailed_action(`device.${action.name}`); + + return this.addMenuItem(item, index); + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Remove a GAction from the top level of the device menu by action name + * + * @param {string} actionName - A GAction name, including scope + * @return {number} The position the item was removed from or -1 + */ + removeMenuAction(actionName) { + try { + const index = this.getMenuAction(actionName); + + if (index > -1) + this.menu.remove(index); + + return index; + } catch (e) { + debug(e, this.name); + return -1; + } + } + + /** + * Withdraw a device notification. + * + * @param {string} id - Id for the notification to withdraw + */ + hideNotification(id) { + if (this.service === null) + return; + + this.service.withdraw_notification(`${this.id}|${id}`); + } + + /** + * Show a device notification. + * + * @param {Object} params - A dictionary of notification parameters + * @param {number} [params.id] - A UNIX epoch timestamp (ms) + * @param {string} [params.title] - A title + * @param {string} [params.body] - A body + * @param {Gio.Icon} [params.icon] - An icon + * @param {Gio.NotificationPriority} [params.priority] - The priority + * @param {Array} [params.actions] - A dictionary of action parameters + * @param {Array} [params.buttons] - An Array of buttons + */ + showNotification(params) { + if (this.service === null) + return; + + // KDE Connect on Android can sometimes give an undefined for params.body + Object.keys(params) + .forEach(key => params[key] === undefined && delete params[key]); + + params = Object.assign({ + id: Date.now(), + title: this.name, + body: '', + icon: new Gio.ThemedIcon({name: this.icon_name}), + priority: Gio.NotificationPriority.NORMAL, + action: null, + buttons: [], + }, params); + + const notif = new Gio.Notification(); + notif.set_title(params.title); + notif.set_body(params.body); + notif.set_icon(params.icon); + notif.set_priority(params.priority); + + // Default Action + if (params.action) { + const hasParameter = (params.action.parameter !== null); + + if (!hasParameter) + params.action.parameter = new GLib.Variant('s', ''); + + notif.set_default_action_and_target( + 'app.device', + new GLib.Variant('(ssbv)', [ + this.id, + params.action.name, + hasParameter, + params.action.parameter, + ]) + ); + } + + // Buttons + for (const button of params.buttons) { + const hasParameter = (button.parameter !== null); + + if (!hasParameter) + button.parameter = new GLib.Variant('s', ''); + + notif.add_button_with_target( + button.label, + 'app.device', + new GLib.Variant('(ssbv)', [ + this.id, + button.action, + hasParameter, + button.parameter, + ]) + ); + } + + this.service.send_notification(`${this.id}|${params.id}`, notif); + } + + /** + * Cancel an ongoing file transfer. + * + * @param {Gio.Action} action - The GAction + * @param {GLib.Variant} parameter - The activation parameter + */ + cancelTransfer(action, parameter) { + try { + const uuid = parameter.unpack(); + const transfer = this._transfers.get(uuid); + + if (transfer === undefined) + return; + + this._transfers.delete(uuid); + transfer.cancel(); + } catch (e) { + logError(e, this.name); + } + } + + /** + * Create a transfer object. + * + * @return {Core.Transfer} A new transfer + */ + createTransfer() { + const transfer = new Core.Transfer({device: this}); + + // Track the transfer + this._transfers.set(transfer.uuid, transfer); + + transfer.connect('notify::completed', (transfer) => { + this._transfers.delete(transfer.uuid); + }); + + return transfer; + } + + /** + * Reject the transfer payload described by @packet. + * + * @param {Core.Packet} packet - A packet + * @return {Promise} A promise for the operation + */ + rejectTransfer(packet) { + if (!packet || !packet.hasPayload()) + return; + + return this.channel.rejectTransfer(packet); + } + + openPath(action, parameter) { + const path = parameter.unpack(); + + // Normalize paths to URIs, assuming local file + const uri = path.includes('://') ? path : `file://${path}`; + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + showPathInFolder(action, parameter) { + const path = parameter.unpack(); + const uri = path.includes('://') ? path : `file://${path}`; + + const connection = Gio.DBus.session; + connection.call( + 'org.freedesktop.FileManager1', + '/org/freedesktop/FileManager1', + 'org.freedesktop.FileManager1', + 'ShowItems', + new GLib.Variant('(ass)', [[uri], 's']), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + logError(e); + } + } + ); + } + + _clearCache(action, parameter) { + for (const plugin of this._plugins.values()) { + try { + plugin.clearCache(); + } catch (e) { + debug(e, this.name); + } + } + } + + /** + * Pair request handler + * + * @param {Core.Packet} packet - A complete kdeconnect.pair packet + */ + _handlePair(packet) { + // A pair has been requested/confirmed + if (packet.body.pair) { + // The device is accepting our request + if (this._outgoingPairRequest) { + this._setPaired(true); + this._loadPlugins(); + + // The device thinks we're unpaired + } else if (this.paired) { + this._setPaired(true); + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + this._triggerPlugins(); + + // The device is requesting pairing + } else { + this._notifyPairRequest(); + } + // Device is requesting unpairing/rejecting our request + } else { + this._setPaired(false); + this._unloadPlugins(); + } + } + + /** + * Notify the user of an incoming pair request and set a 30s timeout + */ + _notifyPairRequest() { + // Reset any active request + this._resetPairRequest(); + + this.showNotification({ + id: 'pair-request', + // TRANSLATORS: eg. Pair Request from Google Pixel + title: _('Pair Request from %s').format(this.name), + body: this.encryption_info, + icon: new Gio.ThemedIcon({name: 'channel-insecure-symbolic'}), + priority: Gio.NotificationPriority.URGENT, + buttons: [ + { + action: 'unpair', + label: _('Reject'), + parameter: null, + }, + { + action: 'pair', + label: _('Accept'), + parameter: null, + }, + ], + }); + + // Start a 30s countdown + this._incomingPairRequest = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + 30, + this._setPaired.bind(this, false) + ); + } + + /** + * Reset pair request timeouts and withdraw any notifications + */ + _resetPairRequest() { + this.hideNotification('pair-request'); + + if (this._incomingPairRequest) { + GLib.source_remove(this._incomingPairRequest); + this._incomingPairRequest = 0; + } + + if (this._outgoingPairRequest) { + GLib.source_remove(this._outgoingPairRequest); + this._outgoingPairRequest = 0; + } + } + + /** + * Set the internal paired state of the device and emit ::notify + * + * @param {boolean} paired - The paired state to set + */ + _setPaired(paired) { + this._resetPairRequest(); + + // For TCP connections we store or reset the TLS Certificate + if (this.connection_type === 'lan') { + if (paired) { + this.settings.set_string( + 'certificate-pem', + this.channel.peer_certificate.certificate_pem + ); + } else { + this.settings.reset('certificate-pem'); + } + } + + // If we've become unpaired, stop all subprocesses and transfers + if (!paired) { + for (const proc of this._procs) + proc.force_exit(); + + this._procs.clear(); + + for (const transfer of this._transfers.values()) + transfer.close(); + + this._transfers.clear(); + } + + this.settings.set_boolean('paired', paired); + this.notify('paired'); + } + + /** + * Send or accept an incoming pair request; also exported as a GAction + */ + pair() { + try { + // If we're accepting an incoming pair request, set the internal + // paired state and send the confirmation before loading plugins. + if (this._incomingPairRequest) { + this._setPaired(true); + + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + + this._loadPlugins(); + + // If we're initiating an outgoing pair request, be sure the timer + // is reset before sending the request and setting a 30s timeout. + } else if (!this.paired) { + this._resetPairRequest(); + + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: true}, + }); + + this._outgoingPairRequest = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + 30, + this._setPaired.bind(this, false) + ); + } + } catch (e) { + logError(e, this.name); + } + } + + /** + * Unpair or reject an incoming pair request; also exported as a GAction + */ + unpair() { + try { + if (this.connected) { + this.sendPacket({ + type: 'kdeconnect.pair', + body: {pair: false}, + }); + } + + this._setPaired(false); + this._unloadPlugins(); + } catch (e) { + logError(e, this.name); + } + } + + /* + * Plugin Functions + */ + _onAllowedPluginsChanged(settings) { + const disabled = this.settings.get_strv('disabled-plugins'); + const supported = this.settings.get_strv('supported-plugins'); + const allowed = supported.filter(name => !disabled.includes(name)); + + // Unload any plugins that are disabled or unsupported + this._plugins.forEach(plugin => { + if (!allowed.includes(plugin.name)) + this._unloadPlugin(plugin.name); + }); + + // Make sure we change the contacts store if the plugin was disabled + if (!allowed.includes('contacts')) + this.notify('contacts'); + + // Load allowed plugins + for (const name of allowed) + this._loadPlugin(name); + } + + _loadPlugin(name) { + let handler, plugin; + + try { + if (this.paired && !this._plugins.has(name)) { + // Instantiate the handler + handler = imports.service.plugins[name]; + plugin = new handler.Plugin(this); + + // Register packet handlers + for (const packetType of handler.Metadata.incomingCapabilities) + this._handlers.set(packetType, plugin); + + // Register plugin + this._plugins.set(name, plugin); + + // Run the connected()/disconnected() handler + if (this.connected) + plugin.connected(); + else + plugin.disconnected(); + } + } catch (e) { + if (plugin !== undefined) + plugin.destroy(); + + if (this.service !== null) + this.service.notify_error(e); + else + logError(e, this.name); + } + } + + async _loadPlugins() { + const disabled = this.settings.get_strv('disabled-plugins'); + + for (const name of this.settings.get_strv('supported-plugins')) { + if (!disabled.includes(name)) + await this._loadPlugin(name); + } + } + + _unloadPlugin(name) { + let handler, plugin; + + try { + if (this._plugins.has(name)) { + // Unregister packet handlers + handler = imports.service.plugins[name]; + + for (const type of handler.Metadata.incomingCapabilities) + this._handlers.delete(type); + + // Unregister plugin + plugin = this._plugins.get(name); + this._plugins.delete(name); + plugin.destroy(); + } + } catch (e) { + logError(e, this.name); + } + } + + async _unloadPlugins() { + for (const name of this._plugins.keys()) + await this._unloadPlugin(name); + } + + _triggerPlugins() { + for (const plugin of this._plugins.values()) { + if (this.connected) + plugin.connected(); + else + plugin.disconnected(); + } + } + + destroy() { + // Drop the default contacts store if we were using it + if (this._contacts !== undefined) + this._contacts = Components.release('contacts'); + + // Close the channel if still connected + if (this.channel !== null) + this.channel.close(); + + // Synchronously destroy plugins + this._plugins.forEach(plugin => plugin.destroy()); + this._plugins.clear(); + + // Dispose GSettings + this.settings.disconnect(this._disabledPluginsChangedId); + this.settings.disconnect(this._supportedPluginsChangedId); + this.settings.run_dispose(); + + GObject.signal_handlers_destroy(this); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/manager.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/manager.js new file mode 100644 index 0000000..9d86268f --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/manager.js @@ -0,0 +1,508 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const DBus = imports.service.utils.dbus; +const Device = imports.service.device; + +const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device'; +const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device'; +const DEVICE_IFACE = Config.DBUS.lookup_interface(DEVICE_NAME); + + +/** + * A manager for devices. + */ +var Manager = GObject.registerClass({ + GTypeName: 'GSConnectManager', + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the manager is active', + GObject.ParamFlags.READABLE, + false + ), + 'discoverable': GObject.ParamSpec.boolean( + 'discoverable', + 'Discoverable', + 'Whether the service responds to discovery requests', + GObject.ParamFlags.READWRITE, + false + ), + 'id': GObject.ParamSpec.string( + 'id', + 'Id', + 'The hostname or other network unique id', + GObject.ParamFlags.READWRITE, + null + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The name announced to the network', + GObject.ParamFlags.READWRITE, + 'GSConnect' + ), + }, +}, class Manager extends Gio.DBusObjectManagerServer { + + _init(params = {}) { + super._init(params); + + this._exported = new WeakMap(); + this._reconnectId = 0; + + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + this._initSettings(); + } + + get active() { + if (this._active === undefined) + this._active = false; + + return this._active; + } + + get backends() { + if (this._backends === undefined) + this._backends = new Map(); + + return this._backends; + } + + get devices() { + if (this._devices === undefined) + this._devices = new Map(); + + return this._devices; + } + + get discoverable() { + if (this._discoverable === undefined) + this._discoverable = this.settings.get_boolean('discoverable'); + + return this._discoverable; + } + + set discoverable(value) { + if (this.discoverable === value) + return; + + this._discoverable = value; + this.notify('discoverable'); + + // FIXME: This whole thing just keeps getting uglier + const application = Gio.Application.get_default(); + + if (application === null) + return; + + if (this.discoverable) { + Gio.Application.prototype.withdraw_notification.call( + application, + 'discovery-warning' + ); + } else { + const notif = new Gio.Notification(); + notif.set_title(_('Discovery Disabled')); + notif.set_body(_('Discovery has been disabled due to the number of devices on this network.')); + notif.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'})); + notif.set_priority(Gio.NotificationPriority.HIGH); + notif.set_default_action('app.preferences'); + + Gio.Application.prototype.withdraw_notification.call( + application, + 'discovery-warning', + notif + ); + } + } + + get id() { + if (this._id === undefined) + this._id = this.settings.get_string('id'); + + return this._id; + } + + set id(value) { + if (this.id === value) + return; + + this._id = value; + this.notify('id'); + } + + get name() { + if (this._name === undefined) + this._name = this.settings.get_string('name'); + + return this._name; + } + + set name(value) { + if (this.name === value) + return; + + this._name = value; + this.notify('name'); + + // Broadcast changes to the network + for (const backend of this.backends.values()) { + backend.name = this.name; + backend.buildIdentity(); + } + + this.identify(); + } + + get settings() { + if (this._settings === undefined) { + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), + }); + } + + return this._settings; + } + + vfunc_notify(pspec) { + if (pspec.name !== 'connection') + return; + + if (this.connection !== null) + this._exportDevices(); + else + this._unexportDevices(); + } + + /* + * GSettings + */ + _initSettings() { + // Initialize the ID and name of the service + if (this.settings.get_string('id').length === 0) + this.settings.set_string('id', GLib.uuid_string_random()); + + if (this.settings.get_string('name').length === 0) + this.settings.set_string('name', GLib.get_host_name()); + + // Bound Properties + this.settings.bind('discoverable', this, 'discoverable', 0); + this.settings.bind('id', this, 'id', 0); + this.settings.bind('name', this, 'name', 0); + } + + /* + * Backends + */ + _onChannel(backend, channel) { + try { + let device = this.devices.get(channel.identity.body.deviceId); + + switch (true) { + // Proceed if this is an existing device... + case (device !== undefined): + break; + + // Or the connection is allowed... + case this.discoverable || channel.allowed: + device = this._ensureDevice(channel.identity); + break; + + // ...otherwise bail + default: + debug(`${channel.identity.body.deviceName}: not allowed`); + return false; + } + + device.setChannel(channel); + return true; + } catch (e) { + logError(e, backend.name); + return false; + } + } + + _loadBackends() { + for (const name in imports.service.backends) { + try { + const module = imports.service.backends[name]; + + if (module.ChannelService === undefined) + continue; + + // Try to create the backend and track it if successful + const backend = new module.ChannelService({ + id: this.id, + name: this.name, + }); + this.backends.set(name, backend); + + // Connect to the backend + backend.__channelId = backend.connect( + 'channel', + this._onChannel.bind(this) + ); + + // Now try to start the backend, allowing us to retry if we fail + backend.start(); + } catch (e) { + if (Gio.Application.get_default()) + Gio.Application.get_default().notify_error(e); + } + } + } + + /* + * Devices + */ + _loadDevices() { + // Load cached devices + for (const id of this.settings.get_strv('devices')) { + const device = new Device.Device({body: {deviceId: id}}); + this._exportDevice(device); + this.devices.set(id, device); + } + } + + _exportDevice(device) { + if (this.connection === null) + return; + + const info = { + object: null, + interface: null, + actions: 0, + menu: 0, + }; + + const objectPath = `${DEVICE_PATH}/${device.id.replace(/\W+/g, '_')}`; + + // Export an object path for the device + info.object = new Gio.DBusObjectSkeleton({ + g_object_path: objectPath, + }); + this.export(info.object); + + // Export GActions & GMenu + info.actions = Gio.DBus.session.export_action_group(objectPath, device); + info.menu = Gio.DBus.session.export_menu_model(objectPath, device.menu); + + // Export the Device interface + info.interface = new DBus.Interface({ + g_instance: device, + g_interface_info: DEVICE_IFACE, + }); + info.object.add_interface(info.interface); + + this._exported.set(device, info); + } + + _exportDevices() { + if (this.connection === null) + return; + + for (const device of this.devices.values()) + this._exportDevice(device); + } + + _unexportDevice(device) { + const info = this._exported.get(device); + + if (info === undefined) + return; + + // Unexport GActions and GMenu + Gio.DBus.session.unexport_action_group(info.actions); + Gio.DBus.session.unexport_menu_model(info.menu); + + // Unexport the Device interface and object + info.interface.flush(); + info.object.remove_interface(info.interface); + info.object.flush(); + this.unexport(info.object.g_object_path); + + this._exported.delete(device); + } + + _unexportDevices() { + for (const device of this.devices.values()) + this._unexportDevice(device); + } + + /** + * Return a device for @packet, creating it and adding it to the list of + * of known devices if it doesn't exist. + * + * @param {Core.Packet} packet - An identity packet for the device + * @return {Device.Device} A device object + */ + _ensureDevice(packet) { + let device = this.devices.get(packet.body.deviceId); + + if (device === undefined) { + debug(`Adding ${packet.body.deviceName}`); + + // TODO: Remove when all clients support bluetooth-like discovery + // + // If this is the third unpaired device to connect, we disable + // discovery to avoid choking on networks with many devices + const unpaired = Array.from(this.devices.values()).filter(dev => { + return !dev.paired; + }); + + if (unpaired.length === 3) + this.discoverable = false; + + device = new Device.Device(packet); + this._exportDevice(device); + this.devices.set(device.id, device); + + // Notify + this.settings.set_strv('devices', Array.from(this.devices.keys())); + } + + return device; + } + + /** + * Permanently remove a device. + * + * Removes the device from the list of known devices, deletes all GSettings + * and files. + * + * @param {string} id - The id of the device to delete + */ + _removeDevice(id) { + // Delete all GSettings + const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`; + GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`); + + // Delete the cache + const cache = GLib.build_filenamev([Config.CACHEDIR, id]); + Gio.File.rm_rf(cache); + + // Forget the device + this.devices.delete(id); + this.settings.set_strv('devices', Array.from(this.devices.keys())); + } + + /** + * A GSourceFunc that tries to reconnect to each paired device, while + * pruning unpaired devices that have disconnected. + * + * @return {boolean} Always %true + */ + _reconnect() { + for (const [id, device] of this.devices) { + if (device.connected) + continue; + + if (device.paired) { + this.identify(device.settings.get_string('last-connection')); + continue; + } + + this._unexportDevice(device); + this._removeDevice(id); + device.destroy(); + } + + return GLib.SOURCE_CONTINUE; + } + + /** + * Identify to an address or broadcast to the network. + * + * @param {string} [uri] - An address URI or %null to broadcast + */ + identify(uri = null) { + try { + // If we're passed a parameter, try and find a backend for it + if (uri !== null) { + const [scheme, address] = uri.split('://'); + + const backend = this.backends.get(scheme); + + if (backend !== undefined) + backend.broadcast(address); + + // If we're not discoverable, only try to reconnect known devices + } else if (!this.discoverable) { + this._reconnect(); + + // Otherwise have each backend broadcast to it's network + } else { + this.backends.forEach(backend => backend.broadcast()); + } + } catch (e) { + logError(e); + } + } + + /** + * Start managing devices. + */ + start() { + if (this.active) + return; + + this._loadDevices(); + this._loadBackends(); + + if (this._reconnectId === 0) { + this._reconnectId = GLib.timeout_add_seconds( + GLib.PRIORITY_LOW, + 5, + this._reconnect.bind(this) + ); + } + + this._active = true; + this.notify('active'); + } + + /** + * Stop managing devices. + */ + stop() { + if (!this.active) + return; + + if (this._reconnectId > 0) { + GLib.Source.remove(this._reconnectId); + this._reconnectId = 0; + } + + this._unexportDevices(); + + this.backends.forEach(backend => backend.destroy()); + this.backends.clear(); + + this.devices.forEach(device => device.destroy()); + this.devices.clear(); + + this._active = false; + this.notify('active'); + } + + /** + * Stop managing devices and free any resources. + */ + destroy() { + this.stop(); + this.set_connection(null); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js new file mode 100755 index 0000000..d83fc7f --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/nativeMessagingHost.js @@ -0,0 +1,219 @@ +#!/usr/bin/env gjs + +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +imports.gi.versions.Gio = '2.0'; +imports.gi.versions.GLib = '2.0'; +imports.gi.versions.GObject = '2.0'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const System = imports.system; + + +const NativeMessagingHost = GObject.registerClass({ + GTypeName: 'GSConnectNativeMessagingHost', +}, class NativeMessagingHost extends Gio.Application { + + _init() { + super._init({ + application_id: 'org.gnome.Shell.Extensions.GSConnect.NativeMessagingHost', + flags: Gio.ApplicationFlags.NON_UNIQUE, + }); + } + + get devices() { + if (this._devices === undefined) + this._devices = {}; + + return this._devices; + } + + vfunc_activate() { + super.vfunc_activate(); + } + + vfunc_startup() { + super.vfunc_startup(); + this.hold(); + + // IO Channels + this._stdin = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({fd: 0}), + byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, + }); + + this._stdout = new Gio.DataOutputStream({ + base_stream: new Gio.UnixOutputStream({fd: 1}), + byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN, + }); + + const source = this._stdin.base_stream.create_source(null); + source.set_callback(this.receive.bind(this)); + source.attach(null); + + // Device Manager + try { + this._manager = Gio.DBusObjectManagerClient.new_for_bus_sync( + Gio.BusType.SESSION, + Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START, + 'org.gnome.Shell.Extensions.GSConnect', + '/org/gnome/Shell/Extensions/GSConnect', + null, + null + ); + } catch (e) { + logError(e); + this.quit(); + } + + // Add currently managed devices + for (const object of this._manager.get_objects()) { + for (const iface of object.get_interfaces()) + this._onInterfaceAdded(this._manager, object, iface); + } + + // Watch for new and removed devices + this._manager.connect( + 'interface-added', + this._onInterfaceAdded.bind(this) + ); + this._manager.connect( + 'object-removed', + this._onObjectRemoved.bind(this) + ); + + // Watch for device property changes + this._manager.connect( + 'interface-proxy-properties-changed', + this.sendDeviceList.bind(this) + ); + + // Watch for service restarts + this._manager.connect( + 'notify::name-owner', + this.sendDeviceList.bind(this) + ); + + this.send({ + type: 'connected', + data: (this._manager.name_owner !== null), + }); + } + + receive() { + try { + // Read the message + const length = this._stdin.read_int32(null); + const bytes = this._stdin.read_bytes(length, null).toArray(); + const message = JSON.parse(new TextDecoder().decode(bytes)); + + // A request for a list of devices + if (message.type === 'devices') { + this.sendDeviceList(); + + // A request to invoke an action + } else if (message.type === 'share') { + let actionName; + const device = this.devices[message.data.device]; + + if (device) { + if (message.data.action === 'share') + actionName = 'shareUri'; + else if (message.data.action === 'telephony') + actionName = 'shareSms'; + + device.actions.activate_action( + actionName, + new GLib.Variant('s', message.data.url) + ); + } + } + + return GLib.SOURCE_CONTINUE; + } catch (e) { + this.quit(); + } + } + + send(message) { + try { + const data = JSON.stringify(message); + this._stdout.put_int32(data.length, null); + this._stdout.put_string(data, null); + } catch (e) { + this.quit(); + } + } + + sendDeviceList() { + // Inform the WebExtension we're disconnected from the service + if (this._manager && this._manager.name_owner === null) + return this.send({type: 'connected', data: false}); + + // Collect all the devices with supported actions + const available = []; + + for (const device of Object.values(this.devices)) { + const share = device.actions.get_action_enabled('shareUri'); + const telephony = device.actions.get_action_enabled('shareSms'); + + if (share || telephony) { + available.push({ + id: device.g_object_path, + name: device.name, + type: device.type, + share: share, + telephony: telephony, + }); + } + } + + this.send({type: 'devices', data: available}); + } + + _proxyGetter(name) { + try { + return this.get_cached_property(name).unpack(); + } catch (e) { + return null; + } + } + + _onInterfaceAdded(manager, object, iface) { + Object.defineProperties(iface, { + 'name': { + get: this._proxyGetter.bind(iface, 'Name'), + enumerable: true, + }, + // TODO: phase this out for icon-name + 'type': { + get: this._proxyGetter.bind(iface, 'Type'), + enumerable: true, + }, + }); + + iface.actions = Gio.DBusActionGroup.get( + iface.g_connection, + iface.g_name, + iface.g_object_path + ); + + this.devices[iface.g_object_path] = iface; + this.sendDeviceList(); + } + + _onObjectRemoved(manager, object) { + delete this.devices[object.g_object_path]; + this.sendDeviceList(); + } +}); + +// NOTE: must not pass ARGV +(new NativeMessagingHost()).run([System.programInvocationName]); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugin.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugin.js new file mode 100644 index 0000000..f096c16 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugin.js @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; + + +/** + * Base class for device plugins. + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPlugin', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device that owns this plugin', + GObject.ParamFlags.READABLE, + GObject.Object + ), + 'name': GObject.ParamSpec.string( + 'name', + 'Name', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Plugin extends GObject.Object { + + _init(device, name, meta = null) { + super._init(); + + this._device = device; + this._name = name; + this._meta = meta; + + if (this._meta === null) + this._meta = imports.service.plugins[name].Metadata; + + // GSettings + const schema = Config.GSCHEMA.lookup(this._meta.id, false); + + if (schema !== null) { + this.settings = new Gio.Settings({ + settings_schema: schema, + path: `${device.settings.path}plugin/${name}/`, + }); + } + + // GActions + this._gactions = []; + + if (this._meta.actions) { + const menu = this.device.settings.get_strv('menu-actions'); + + for (const name in this._meta.actions) { + const info = this._meta.actions[name]; + this._registerAction(name, menu.indexOf(name), info); + } + } + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get device() { + return this._device; + } + + get name() { + return this._name; + } + + _activateAction(action, parameter) { + try { + let args = null; + + if (parameter instanceof GLib.Variant) + args = parameter.full_unpack(); + + if (Array.isArray(args)) + this[action.name](...args); + else + this[action.name](args); + } catch (e) { + logError(e, action.name); + } + } + + _registerAction(name, menuIndex, info) { + try { + // Device Action + const action = new Gio.SimpleAction({ + name: name, + parameter_type: info.parameter_type, + enabled: false, + }); + action.connect('activate', this._activateAction.bind(this)); + + this.device.add_action(action); + + // Menu + if (menuIndex > -1) { + this.device.addMenuAction( + action, + menuIndex, + info.label, + info.icon_name + ); + } + + this._gactions.push(action); + } catch (e) { + logError(e, `${this.device.name}: ${this.name}`); + } + } + + /** + * Called when the device connects. + */ + connected() { + // Enabled based on device capabilities, which might change + const incoming = this.device.settings.get_strv('incoming-capabilities'); + const outgoing = this.device.settings.get_strv('outgoing-capabilities'); + + for (const action of this._gactions) { + const info = this._meta.actions[action.name]; + + if (info.incoming.every(type => outgoing.includes(type)) && + info.outgoing.every(type => incoming.includes(type))) + action.set_enabled(true); + } + } + + /** + * Called when the device disconnects. + */ + disconnected() { + for (const action of this._gactions) + action.set_enabled(false); + } + + /** + * Called when a packet is received that the plugin is a handler for. + * + * @param {Core.Packet} packet - A KDE Connect packet + */ + handlePacket(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Cache JSON parseable properties on this object for persistence. The + * filename ~/.cache/gsconnect/<device-id>/<plugin-name>.json will be used + * to store the properties and values. + * + * Calling cacheProperties() opens a JSON cache file and reads any stored + * properties and values onto the current instance. When destroy() + * is called the properties are automatically stored in the same file. + * + * @param {Array} names - A list of this object's property names to cache + */ + async cacheProperties(names) { + try { + this._cacheProperties = names; + + // Ensure the device's cache directory exists + const cachedir = GLib.build_filenamev([ + Config.CACHEDIR, + this.device.id, + ]); + GLib.mkdir_with_parents(cachedir, 448); + + this._cacheFile = Gio.File.new_for_path( + GLib.build_filenamev([cachedir, `${this.name}.json`])); + + // Read the cache from disk + const [contents] = await this._cacheFile.load_contents_async( + this.cancellable); + const cache = JSON.parse(new TextDecoder().decode(contents)); + Object.assign(this, cache); + } catch (e) { + debug(e.message, `${this.device.name}: ${this.name}`); + } finally { + this.cacheLoaded(); + } + } + + /** + * An overridable function that is invoked when the on-disk cache is being + * cleared. Implementations should use this function to clear any in-memory + * cache data. + */ + clearCache() {} + + /** + * An overridable function that is invoked when the cache is done loading + */ + cacheLoaded() {} + + /** + * Unregister plugin actions, write the cache (if applicable) and destroy + * any dangling signal handlers. + */ + destroy() { + // Cancel any pending plugin operations + if (this._cancellable !== undefined) + this._cancellable.cancel(); + + for (const action of this._gactions) { + this.device.removeMenuAction(`device.${action.name}`); + this.device.remove_action(action.name); + } + + // Write the cache to disk synchronously + if (this._cacheFile !== undefined) { + try { + // Build the cache + const cache = {}; + + for (const name of this._cacheProperties) + cache[name] = this[name]; + + this._cacheFile.replace_contents( + JSON.stringify(cache, null, 2), + null, + false, + Gio.FileCreateFlags.REPLACE_DESTINATION, + null + ); + } catch (e) { + debug(e.message, `${this.device.name}: ${this.name}`); + } + } + + GObject.signal_handlers_destroy(this); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js new file mode 100644 index 0000000..7727013 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js @@ -0,0 +1,433 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Battery'), + description: _('Exchange battery information'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Battery', + incomingCapabilities: [ + 'kdeconnect.battery', + 'kdeconnect.battery.request', + ], + outgoingCapabilities: [ + 'kdeconnect.battery', + 'kdeconnect.battery.request', + ], + actions: {}, +}; + + +/** + * Battery Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/battery + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectBatteryPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'battery'); + + // Setup Cache; defaults are 90 minute charge, 1 day discharge + this._chargeState = [54, 0, -1]; + this._dischargeState = [864, 0, -1]; + this._thresholdLevel = 25; + + this.cacheProperties([ + '_chargeState', + '_dischargeState', + '_thresholdLevel', + ]); + + // Export battery state as GAction + this.__state = new Gio.SimpleAction({ + name: 'battery', + parameter_type: new GLib.VariantType('(bsii)'), + state: this.state, + }); + this.device.add_action(this.__state); + + // Local Battery (UPower) + this._upower = null; + this._sendStatisticsId = this.settings.connect( + 'changed::send-statistics', + this._onSendStatisticsChanged.bind(this) + ); + this._onSendStatisticsChanged(this.settings); + } + + get charging() { + if (this._charging === undefined) + this._charging = false; + + return this._charging; + } + + get icon_name() { + let icon; + + if (this.level === -1) + return 'battery-missing-symbolic'; + else if (this.level === 100) + return 'battery-full-charged-symbolic'; + else if (this.level < 3) + icon = 'battery-empty'; + else if (this.level < 10) + icon = 'battery-caution'; + else if (this.level < 30) + icon = 'battery-low'; + else if (this.level < 60) + icon = 'battery-good'; + else if (this.level >= 60) + icon = 'battery-full'; + + if (this.charging) + return `${icon}-charging-symbolic`; + + return `${icon}-symbolic`; + } + + get level() { + // This is what KDE Connect returns if the remote battery plugin is + // disabled or still being loaded + if (this._level === undefined) + this._level = -1; + + return this._level; + } + + get time() { + if (this._time === undefined) + this._time = 0; + + return this._time; + } + + get state() { + return new GLib.Variant( + '(bsii)', + [this.charging, this.icon_name, this.level, this.time] + ); + } + + cacheLoaded() { + this._initEstimate(); + this._sendState(); + } + + clearCache() { + this._chargeState = [54, 0, -1]; + this._dischargeState = [864, 0, -1]; + this._thresholdLevel = 25; + this._initEstimate(); + } + + connected() { + super.connected(); + + this._requestState(); + this._sendState(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.battery': + this._receiveState(packet); + break; + + case 'kdeconnect.battery.request': + this._sendState(); + break; + } + } + + _onSendStatisticsChanged() { + if (this.settings.get_boolean('send-statistics')) + this._monitorState(); + else + this._unmonitorState(); + } + + /** + * Recalculate and update the estimated time remaining, but not the rate. + */ + _initEstimate() { + let rate, level; + + // elision of [rate, time, level] + if (this.charging) + [rate,, level] = this._chargeState; + else + [rate,, level] = this._dischargeState; + + if (!Number.isFinite(rate) || rate < 1) + rate = this.charging ? 864 : 90; + + if (!Number.isFinite(level) || level < 0) + level = this.level; + + // Update the time remaining + if (rate && this.charging) + this._time = Math.floor(rate * (100 - level)); + else if (rate && !this.charging) + this._time = Math.floor(rate * level); + + this.__state.state = this.state; + } + + /** + * Recalculate the (dis)charge rate and update the estimated time remaining. + */ + _updateEstimate() { + let rate, time, level; + const newTime = Math.floor(Date.now() / 1000); + const newLevel = this.level; + + // Load the state; ensure we have sane values for calculation + if (this.charging) + [rate, time, level] = this._chargeState; + else + [rate, time, level] = this._dischargeState; + + if (!Number.isFinite(rate) || rate < 1) + rate = this.charging ? 54 : 864; + + if (!Number.isFinite(time) || time <= 0) + time = newTime; + + if (!Number.isFinite(level) || level < 0) + level = newLevel; + + // Update the rate; use a weighted average to account for missed changes + // NOTE: (rate = seconds/percent) + const ldiff = this.charging ? newLevel - level : level - newLevel; + const tdiff = newTime - time; + const newRate = tdiff / ldiff; + + if (newRate && Number.isFinite(newRate)) + rate = Math.floor((rate * 0.4) + (newRate * 0.6)); + + // Store the state for the next recalculation + if (this.charging) + this._chargeState = [rate, newTime, newLevel]; + else + this._dischargeState = [rate, newTime, newLevel]; + + // Update the time remaining + if (rate && this.charging) + this._time = Math.floor(rate * (100 - newLevel)); + else if (rate && !this.charging) + this._time = Math.floor(rate * newLevel); + + this.__state.state = this.state; + } + + /** + * Notify the user the remote battery is full. + */ + _fullBatteryNotification() { + if (!this.settings.get_boolean('full-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|full', + // TRANSLATORS: eg. Google Pixel: Battery is full + title: _('%s: Battery is full').format(this.device.name), + // TRANSLATORS: when the battery is fully charged + body: _('Fully Charged'), + icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'), + buttons: buttons, + }); + } + + /** + * Notify the user the remote battery is at custom charge level. + */ + _customBatteryNotification() { + if (!this.settings.get_boolean('custom-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|custom', + // TRANSLATORS: eg. Google Pixel: Battery has reached custom charge level + title: _('%s: Battery has reached custom charge level').format(this.device.name), + // TRANSLATORS: when the battery has reached custom charge level + body: _('%d%% Charged').format(this.level), + icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'), + buttons: buttons, + }); + } + + /** + * Notify the user the remote battery is low. + */ + _lowBatteryNotification() { + if (!this.settings.get_boolean('low-battery-notification')) + return; + + // Offer the option to ring the device, if available + let buttons = []; + + if (this.device.get_action_enabled('ring')) { + buttons = [{ + label: _('Ring'), + action: 'ring', + parameter: null, + }]; + } + + this.device.showNotification({ + id: 'battery|low', + // TRANSLATORS: eg. Google Pixel: Battery is low + title: _('%s: Battery is low').format(this.device.name), + // TRANSLATORS: eg. 15% remaining + body: _('%d%% remaining').format(this.level), + icon: Gio.ThemedIcon.new('battery-caution-symbolic'), + buttons: buttons, + }); + } + + /** + * Handle a remote battery update. + * + * @param {Core.Packet} packet - A kdeconnect.battery packet + */ + _receiveState(packet) { + // Charging state changed + this._charging = packet.body.isCharging; + + // Level changed + if (this._level !== packet.body.currentCharge) { + this._level = packet.body.currentCharge; + + // If the level is above the threshold hide the notification + if (this._level > this._thresholdLevel) + this.device.hideNotification('battery|low'); + + // The level just changed to/from custom level while charging + if ((this._level === this.settings.get_uint('custom-battery-notification-value')) && this._charging) + this._customBatteryNotification(); + else + this.device.hideNotification('battery|custom'); + + // The level just changed to/from full + if (this._level === 100) + this._fullBatteryNotification(); + else + this.device.hideNotification('battery|full'); + } + + // Device considers the level low + if (packet.body.thresholdEvent > 0) { + this._lowBatteryNotification(); + this._thresholdLevel = this.level; + } + + this._updateEstimate(); + } + + /** + * Request the remote battery's current state + */ + _requestState() { + this.device.sendPacket({ + type: 'kdeconnect.battery.request', + body: {request: true}, + }); + } + + /** + * Report the local battery's current state + */ + _sendState() { + if (this._upower === null || !this._upower.is_present) + return; + + this.device.sendPacket({ + type: 'kdeconnect.battery', + body: { + currentCharge: this._upower.level, + isCharging: this._upower.charging, + thresholdEvent: this._upower.threshold, + }, + }); + } + + /* + * UPower monitoring methods + */ + _monitorState() { + try { + // Currently only true if the remote device is a desktop (rare) + const incoming = this.device.settings.get_strv('incoming-capabilities'); + + if (!incoming.includes('kdeconnect.battery')) + return; + + this._upower = Components.acquire('upower'); + + this._upowerId = this._upower.connect( + 'changed', + this._sendState.bind(this) + ); + + this._sendState(); + } catch (e) { + logError(e, this.device.name); + this._unmonitorState(); + } + } + + _unmonitorState() { + try { + if (this._upower === null) + return; + + this._upower.disconnect(this._upowerId); + this._upower = Components.release('upower'); + } catch (e) { + logError(e, this.device.name); + } + } + + destroy() { + this.device.remove_action('battery'); + this.settings.disconnect(this._sendStatisticsId); + this._unmonitorState(); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js new file mode 100644 index 0000000..a08125e --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/clipboard.js @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Clipboard'), + description: _('Share the clipboard content'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Clipboard', + incomingCapabilities: [ + 'kdeconnect.clipboard', + 'kdeconnect.clipboard.connect', + ], + outgoingCapabilities: [ + 'kdeconnect.clipboard', + 'kdeconnect.clipboard.connect', + ], + actions: { + clipboardPush: { + label: _('Clipboard Push'), + icon_name: 'edit-paste-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.clipboard'], + }, + clipboardPull: { + label: _('Clipboard Pull'), + icon_name: 'edit-copy-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.clipboard'], + outgoing: [], + }, + }, +}; + + +/** + * Clipboard Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/clipboard + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectClipboardPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'clipboard'); + + this._clipboard = Components.acquire('clipboard'); + + // Watch local clipboard for changes + this._textChangedId = this._clipboard.connect( + 'notify::text', + this._onLocalClipboardChanged.bind(this) + ); + + // Buffer content to allow selective sync + this._localBuffer = this._clipboard.text; + this._localTimestamp = 0; + this._remoteBuffer = null; + } + + connected() { + super.connected(); + + // TODO: if we're not auto-syncing local->remote, but we are doing the + // reverse, it's possible older remote content will end up + // overwriting newer local content. + if (!this.settings.get_boolean('send-content')) + return; + + if (this._localBuffer === null && this._localTimestamp === 0) + return; + + this.device.sendPacket({ + type: 'kdeconnect.clipboard.connect', + body: { + content: this._localBuffer, + timestamp: this._localTimestamp, + }, + }); + } + + handlePacket(packet) { + if (!packet.body.hasOwnProperty('content')) + return; + + switch (packet.type) { + case 'kdeconnect.clipboard': + this._handleContent(packet); + break; + + case 'kdeconnect.clipboard.connect': + this._handleConnectContent(packet); + break; + } + } + + _handleContent(packet) { + this._onRemoteClipboardChanged(packet.body.content); + } + + _handleConnectContent(packet) { + if (packet.body.hasOwnProperty('timestamp') && + packet.body.timestamp > this._localTimestamp) + this._onRemoteClipboardChanged(packet.body.content); + } + + /* + * Store the local clipboard content and forward it if enabled + */ + _onLocalClipboardChanged(clipboard, pspec) { + this._localBuffer = clipboard.text; + this._localTimestamp = Date.now(); + + if (this.settings.get_boolean('send-content')) + this.clipboardPush(); + } + + /* + * Store the remote clipboard content and apply it if enabled + */ + _onRemoteClipboardChanged(text) { + this._remoteBuffer = text; + + if (this.settings.get_boolean('receive-content')) + this.clipboardPull(); + } + + /** + * Copy to the remote clipboard; called by _onLocalClipboardChanged() + */ + clipboardPush() { + // Don't sync if the clipboard is empty or not text + if (this._localTimestamp === 0) + return; + + if (this._remoteBuffer !== this._localBuffer) { + this._remoteBuffer = this._localBuffer; + + // If the buffer is %null, the clipboard contains non-text content, + // so we neither clear the remote clipboard nor pass the content + if (this._localBuffer !== null) { + this.device.sendPacket({ + type: 'kdeconnect.clipboard', + body: { + content: this._localBuffer, + }, + }); + } + } + } + + /** + * Copy from the remote clipboard; called by _onRemoteClipboardChanged() + */ + clipboardPull() { + if (this._localBuffer !== this._remoteBuffer) { + this._localBuffer = this._remoteBuffer; + this._localTimestamp = Date.now(); + + this._clipboard.text = this._remoteBuffer; + } + } + + destroy() { + if (this._clipboard && this._textChangedId) { + this._clipboard.disconnect(this._textChangedId); + this._clipboard = Components.release('clipboard'); + } + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js new file mode 100644 index 0000000..9aaddb2 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/connectivity_report.js @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Connectivity Report'), + description: _('Display connectivity status'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.ConnectivityReport', + incomingCapabilities: [ + 'kdeconnect.connectivity_report', + ], + outgoingCapabilities: [ + 'kdeconnect.connectivity_report.request', + ], + actions: {}, +}; + + +/** + * Connectivity Report Plugin + * https://invent.kde.org/network/kdeconnect-kde/-/tree/master/plugins/connectivity_report + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectConnectivityReportPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'connectivity_report'); + + // Export connectivity state as GAction + this.__state = new Gio.SimpleAction({ + name: 'connectivityReport', + // ( + // cellular_network_type, + // cellular_network_type_icon, + // cellular_network_strength(0..4), + // cellular_network_strength_icon, + // ) + parameter_type: new GLib.VariantType('(ssis)'), + state: this.state, + }); + this.device.add_action(this.__state); + } + + get signal_strength() { + if (this._signalStrength === undefined) + this._signalStrength = -1; + + return this._signalStrength; + } + + get network_type() { + if (this._networkType === undefined) + this._networkType = ''; + + return this._networkType; + } + + get signal_strength_icon_name() { + if (this.signal_strength === 0) + return 'network-cellular-signal-none-symbolic'; // SIGNAL_STRENGTH_NONE_OR_UNKNOWN + else if (this.signal_strength === 1) + return 'network-cellular-signal-weak-symbolic'; // SIGNAL_STRENGTH_POOR + else if (this.signal_strength === 2) + return 'network-cellular-signal-ok-symbolic'; // SIGNAL_STRENGTH_MODERATE + else if (this.signal_strength === 3) + return 'network-cellular-signal-good-symbolic'; // SIGNAL_STRENGTH_GOOD + else if (this.signal_strength >= 4) + return 'network-cellular-signal-excellent-symbolic'; // SIGNAL_STRENGTH_GREAT + + return 'network-cellular-offline-symbolic'; // OFF (signal_strength == -1) + } + + get network_type_icon_name() { + if (this.network_type === 'GSM' || this.network_type === 'CDMA' || this.network_type === 'iDEN') + return 'network-cellular-2g-symbolic'; + else if (this.network_type === 'UMTS' || this.network_type === 'CDMA2000') + return 'network-cellular-3g-symbolic'; + else if (this.network_type === 'LTE') + return 'network-cellular-4g-symbolic'; + else if (this.network_type === 'EDGE') + return 'network-cellular-edge-symbolic'; + else if (this.network_type === 'GPRS') + return 'network-cellular-gprs-symbolic'; + else if (this.network_type === 'HSPA') + return 'network-cellular-hspa-symbolic'; + else if (this.network_type === '5G') + return 'network-cellular-5g-symbolic'; + + return 'network-cellular-symbolic'; + } + + get state() { + return new GLib.Variant( + '(ssis)', + [ + this.network_type, + this.network_type_icon_name, + this.signal_strength, + this.signal_strength_icon_name, + ] + ); + } + + connected() { + super.connected(); + + this._requestState(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.connectivity_report': + this._receiveState(packet); + break; + } + } + + /** + * Handle a remote state update. + * + * @param {Core.Packet} packet - A kdeconnect.connectivity_report packet + */ + _receiveState(packet) { + if (packet.body.signalStrengths) { + // TODO: Only first SIM (subscriptionID) is supported at the moment + const subs = Object.keys(packet.body.signalStrengths); + const firstSub = Math.min.apply(null, subs); + const data = packet.body.signalStrengths[firstSub]; + + this._networkType = data.networkType; + this._signalStrength = data.signalStrength; + } + + // Update DBus state + this.__state.state = this.state; + } + + /** + * Request the remote device's connectivity state + */ + _requestState() { + this.device.sendPacket({ + type: 'kdeconnect.connectivity_report.request', + body: {}, + }); + } + + destroy() { + this.device.remove_action('connectivity_report'); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js new file mode 100644 index 0000000..f439961 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/contacts.js @@ -0,0 +1,460 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; +const Contacts = imports.service.components.contacts; + +/* + * We prefer libebook's vCard parser if it's available + */ +var EBookContacts; + +try { + EBookContacts = imports.gi.EBookContacts; +} catch (e) { + EBookContacts = null; +} + + +var Metadata = { + label: _('Contacts'), + description: _('Access contacts of the paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Contacts', + incomingCapabilities: [ + 'kdeconnect.contacts.response_uids_timestamps', + 'kdeconnect.contacts.response_vcards', + ], + outgoingCapabilities: [ + 'kdeconnect.contacts.request_all_uids_timestamps', + 'kdeconnect.contacts.request_vcards_by_uid', + ], + actions: {}, +}; + + +/* + * vCard 2.1 Patterns + */ +const VCARD_FOLDING = /\r\n |\r |\n |=\n/g; +const VCARD_SUPPORTED = /^fn|tel|photo|x-kdeconnect/i; +const VCARD_BASIC = /^([^:;]+):(.+)$/; +const VCARD_TYPED = /^([^:;]+);([^:]+):(.+)$/; +const VCARD_TYPED_KEY = /item\d{1,2}\./; +const VCARD_TYPED_META = /([a-z]+)=(.*)/i; + + +/** + * Contacts Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/contacts + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectContactsPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'contacts'); + + this._store = new Contacts.Store(device.id); + this._store.fetch = this._requestUids.bind(this); + + // Notify when the store is ready + this._contactsStoreReadyId = this._store.connect( + 'notify::context', + () => this.device.notify('contacts') + ); + + // Notify if the contacts source changes + this._contactsSourceChangedId = this.settings.connect( + 'changed::contacts-source', + () => this.device.notify('contacts') + ); + + // Load the cache + this._store.load(); + } + + clearCache() { + this._store.clear(); + } + + connected() { + super.connected(); + this._requestUids(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.contacts.response_uids_timestamps': + this._handleUids(packet); + break; + + case 'kdeconnect.contacts.response_vcards': + this._handleVCards(packet); + break; + } + } + + _handleUids(packet) { + try { + const contacts = this._store.contacts; + const remote_uids = packet.body.uids; + let removed = false; + delete packet.body.uids; + + // Usually a failed request, so avoid wiping the cache + if (remote_uids.length === 0) + return; + + // Delete any contacts that were removed on the device + for (let i = 0, len = contacts.length; i < len; i++) { + const contact = contacts[i]; + + if (!remote_uids.includes(contact.id)) { + this._store.remove(contact.id, false); + removed = true; + } + } + + // Build a list of new or updated contacts + const uids = []; + + for (const [uid, timestamp] of Object.entries(packet.body)) { + const contact = this._store.get_contact(uid); + + if (!contact || contact.timestamp !== timestamp) + uids.push(uid); + } + + // Send a request for any new or updated contacts + if (uids.length) + this._requestVCards(uids); + + // If we removed any contacts, save the cache + if (removed) + this._store.save(); + } catch (e) { + logError(e); + } + } + + /** + * Decode a string encoded as "QUOTED-PRINTABLE" and return a regular string + * + * See: https://github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js + * + * @param {string} input - The QUOTED-PRINTABLE string + * @return {string} The decoded string + */ + _decodeQuotedPrintable(input) { + return input + // https://tools.ietf.org/html/rfc2045#section-6.7, rule 3 + .replace(/[\t\x20]$/gm, '') + // Remove hard line breaks preceded by `=` + .replace(/=(?:\r\n?|\n|$)/g, '') + // https://tools.ietf.org/html/rfc2045#section-6.7, note 1. + .replace(/=([a-fA-F0-9]{2})/g, ($0, $1) => { + const codePoint = parseInt($1, 16); + return String.fromCharCode(codePoint); + }); + } + + /** + * Decode a string encoded as "UTF-8" and return a regular string + * + * See: https://github.com/kvz/locutus/blob/master/src/php/xml/utf8_decode.js + * + * @param {string} input - The UTF-8 string + * @return {string} The decoded string + */ + _decodeUTF8(input) { + try { + const output = []; + let i = 0; + let c1 = 0; + let seqlen = 0; + + while (i < input.length) { + c1 = input.charCodeAt(i) & 0xFF; + seqlen = 0; + + if (c1 <= 0xBF) { + c1 &= 0x7F; + seqlen = 1; + } else if (c1 <= 0xDF) { + c1 &= 0x1F; + seqlen = 2; + } else if (c1 <= 0xEF) { + c1 &= 0x0F; + seqlen = 3; + } else { + c1 &= 0x07; + seqlen = 4; + } + + for (let ai = 1; ai < seqlen; ++ai) + c1 = ((c1 << 0x06) | (input.charCodeAt(ai + i) & 0x3F)); + + if (seqlen === 4) { + c1 -= 0x10000; + output.push(String.fromCharCode(0xD800 | ((c1 >> 10) & 0x3FF))); + output.push(String.fromCharCode(0xDC00 | (c1 & 0x3FF))); + } else { + output.push(String.fromCharCode(c1)); + } + + i += seqlen; + } + + return output.join(''); + + // Fallback to old unfaithful + } catch (e) { + try { + return decodeURIComponent(escape(input)); + + // Say "chowdah" frenchie! + } catch (e) { + debug(e, `Failed to decode UTF-8 VCard field ${input}`); + return input; + } + } + } + + /** + * Parse a vCard (v2.1 only) and return a dictionary of the fields + * + * See: http://jsfiddle.net/ARTsinn/P2t2P/ + * + * @param {string} vcard_data - The raw VCard data + * @return {Object} dictionary of vCard data + */ + _parseVCard21(vcard_data) { + // vcard skeleton + const vcard = { + fn: _('Unknown Contact'), + tel: [], + }; + + // Remove line folding and split + const unfolded = vcard_data.replace(VCARD_FOLDING, ''); + const lines = unfolded.split(/\r\n|\r|\n/); + + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + let results, key, type, value; + + // Empty line or a property we aren't interested in + if (!line || !line.match(VCARD_SUPPORTED)) + continue; + + // Basic Fields (fn, x-kdeconnect-timestamp, etc) + if ((results = line.match(VCARD_BASIC))) { + [, key, value] = results; + vcard[key.toLowerCase()] = value; + continue; + } + + // Typed Fields (tel, adr, etc) + if ((results = line.match(VCARD_TYPED))) { + [, key, type, value] = results; + key = key.replace(VCARD_TYPED_KEY, '').toLowerCase(); + value = value.split(';'); + type = type.split(';'); + + // Type(s) + const meta = {}; + + for (let i = 0, len = type.length; i < len; i++) { + const res = type[i].match(VCARD_TYPED_META); + + if (res) + meta[res[1]] = res[2]; + else + meta[`type${i === 0 ? '' : i}`] = type[i].toLowerCase(); + } + + // Value(s) + if (vcard[key] === undefined) + vcard[key] = []; + + // Decode QUOTABLE-PRINTABLE + if (meta.ENCODING && meta.ENCODING === 'QUOTED-PRINTABLE') { + delete meta.ENCODING; + value = value.map(v => this._decodeQuotedPrintable(v)); + } + + // Decode UTF-8 + if (meta.CHARSET && meta.CHARSET === 'UTF-8') { + delete meta.CHARSET; + value = value.map(v => this._decodeUTF8(v)); + } + + // Special case for FN (full name) + if (key === 'fn') + vcard[key] = value[0]; + else + vcard[key].push({meta: meta, value: value}); + } + } + + return vcard; + } + + /** + * Parse a vCard (v2.1 only) using native JavaScript and add it to the + * contact store. + * + * @param {string} uid - The contact UID + * @param {string} vcard_data - The raw vCard data + */ + async _parseVCardNative(uid, vcard_data) { + try { + const vcard = this._parseVCard21(vcard_data); + + const contact = { + id: uid, + name: vcard.fn, + numbers: [], + origin: 'device', + timestamp: parseInt(vcard['x-kdeconnect-timestamp']), + }; + + // Phone Numbers + contact.numbers = vcard.tel.map(entry => { + let type = 'unknown'; + + if (entry.meta && entry.meta.type) + type = entry.meta.type; + + return {type: type, value: entry.value[0]}; + }); + + // Avatar + if (vcard.photo) { + const data = GLib.base64_decode(vcard.photo[0].value[0]); + contact.avatar = await this._store.storeAvatar(data); + } + + this._store.add(contact); + } catch (e) { + debug(e, `Failed to parse VCard contact ${uid}`); + } + } + + /** + * Parse a vCard using libebook and add it to the contact store. + * + * @param {string} uid - The contact UID + * @param {string} vcard_data - The raw vCard data + */ + async _parseVCard(uid, vcard_data) { + try { + const contact = { + id: uid, + name: _('Unknown Contact'), + numbers: [], + origin: 'device', + timestamp: 0, + }; + + const evcard = EBookContacts.VCard.new_from_string(vcard_data); + const attrs = evcard.get_attributes(); + + for (let i = 0, len = attrs.length; i < len; i++) { + const attr = attrs[i]; + let data, number; + + switch (attr.get_name().toLowerCase()) { + case 'fn': + contact.name = attr.get_value(); + break; + + case 'tel': + number = {value: attr.get_value(), type: 'unknown'}; + + if (attr.has_type('CELL')) + number.type = 'cell'; + else if (attr.has_type('HOME')) + number.type = 'home'; + else if (attr.has_type('WORK')) + number.type = 'work'; + + contact.numbers.push(number); + break; + + case 'x-kdeconnect-timestamp': + contact.timestamp = parseInt(attr.get_value()); + break; + + case 'photo': + data = GLib.base64_decode(attr.get_value()); + contact.avatar = await this._store.storeAvatar(data); + break; + } + } + + this._store.add(contact); + } catch (e) { + debug(e, `Failed to parse VCard contact ${uid}`); + } + } + + /** + * Handle an incoming list of contact vCards and pass them to the best + * available parser. + * + * @param {Core.Packet} packet - A `kdeconnect.contacts.response_vcards` + */ + _handleVCards(packet) { + try { + // We don't use this + delete packet.body.uids; + + // Parse each vCard and add the contact + for (const [uid, vcard] of Object.entries(packet.body)) { + if (EBookContacts) + this._parseVCard(uid, vcard); + else + this._parseVCardNative(uid, vcard); + } + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Request a list of contact UIDs with timestamps. + */ + _requestUids() { + this.device.sendPacket({ + type: 'kdeconnect.contacts.request_all_uids_timestamps', + }); + } + + /** + * Request the vCards for @uids. + * + * @param {string[]} uids - A list of contact UIDs + */ + _requestVCards(uids) { + this.device.sendPacket({ + type: 'kdeconnect.contacts.request_vcards_by_uid', + body: { + uids: uids, + }, + }); + } + + destroy() { + this._store.disconnect(this._contactsStoreReadyId); + this.settings.disconnect(this._contactsSourceChangedId); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js new file mode 100644 index 0000000..87514de --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/findmyphone.js @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Find My Phone'), + description: _('Ring your paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.FindMyPhone', + incomingCapabilities: ['kdeconnect.findmyphone.request'], + outgoingCapabilities: ['kdeconnect.findmyphone.request'], + actions: { + ring: { + label: _('Ring'), + icon_name: 'phonelink-ring-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.findmyphone.request'], + }, + }, +}; + + +/** + * FindMyPhone Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/findmyphone + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectFindMyPhonePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'findmyphone'); + + this._dialog = null; + this._player = Components.acquire('sound'); + this._mixer = Components.acquire('pulseaudio'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.findmyphone.request': + this._handleRequest(); + break; + } + } + + /** + * Handle an incoming location request. + */ + _handleRequest() { + try { + // If this is a second request, stop announcing and return + if (this._dialog !== null) { + this._dialog.response(Gtk.ResponseType.DELETE_EVENT); + return; + } + + this._dialog = new Dialog({ + device: this.device, + plugin: this, + }); + + this._dialog.connect('response', () => { + this._dialog = null; + }); + } catch (e) { + this._cancelRequest(); + logError(e, this.device.name); + } + } + + /** + * Cancel any ongoing ringing and destroy the dialog. + */ + _cancelRequest() { + if (this._dialog !== null) + this._dialog.response(Gtk.ResponseType.DELETE_EVENT); + } + + /** + * Request that the remote device announce it's location + */ + ring() { + this.device.sendPacket({ + type: 'kdeconnect.findmyphone.request', + body: {}, + }); + } + + destroy() { + this._cancelRequest(); + + if (this._mixer !== undefined) + this._mixer = Components.release('pulseaudio'); + + if (this._player !== undefined) + this._player = Components.release('sound'); + + super.destroy(); + } +}); + + +/* + * Used to ensure 'audible-bell' is enabled for fallback + */ +const _WM_SETTINGS = new Gio.Settings({ + schema_id: 'org.gnome.desktop.wm.preferences', + path: '/org/gnome/desktop/wm/preferences/', +}); + + +/** + * A custom GtkMessageDialog for alerting of incoming requests + */ +const Dialog = GObject.registerClass({ + GTypeName: 'GSConnectFindMyPhoneDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, +}, class Dialog extends Gtk.MessageDialog { + _init(params) { + super._init({ + buttons: Gtk.ButtonsType.CLOSE, + device: params.device, + image: new Gtk.Image({ + icon_name: 'phonelink-ring-symbolic', + pixel_size: 512, + halign: Gtk.Align.CENTER, + hexpand: true, + valign: Gtk.Align.CENTER, + vexpand: true, + visible: true, + }), + plugin: params.plugin, + urgency_hint: true, + }); + + this.set_keep_above(true); + this.maximize(); + this.message_area.destroy(); + + // If an output stream is available start fading the volume up + if (this.plugin._mixer && this.plugin._mixer.output) { + this._stream = this.plugin._mixer.output; + + this._previousMuted = this._stream.muted; + this._previousVolume = this._stream.volume; + + this._stream.muted = false; + this._stream.fade(0.85, 15); + + // Otherwise ensure audible-bell is enabled + } else { + this._previousBell = _WM_SETTINGS.get_boolean('audible-bell'); + _WM_SETTINGS.set_boolean('audible-bell', true); + } + + // Start the alarm + if (this.plugin._player !== undefined) + this.plugin._player.loopSound('phone-incoming-call', this.cancellable); + + // Show the dialog + this.show_all(); + } + + vfunc_key_press_event(event) { + this.response(Gtk.ResponseType.DELETE_EVENT); + + return Gdk.EVENT_STOP; + } + + vfunc_motion_notify_event(event) { + this.response(Gtk.ResponseType.DELETE_EVENT); + + return Gdk.EVENT_STOP; + } + + vfunc_response(response_id) { + // Stop the alarm + this.cancellable.cancel(); + + // Restore the mixer level + if (this._stream) { + this._stream.muted = this._previousMuted; + this._stream.fade(this._previousVolume); + + // Restore the audible-bell + } else { + _WM_SETTINGS.set_boolean('audible-bell', this._previousBell); + } + + this.destroy(); + } + + get cancellable() { + if (this._cancellable === undefined) + this._cancellable = new Gio.Cancellable(); + + return this._cancellable; + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js new file mode 100644 index 0000000..969eb06 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mousepad.js @@ -0,0 +1,381 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const {InputDialog} = imports.service.ui.mousepad; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Mousepad'), + description: _('Enables the paired device to act as a remote mouse and keyboard'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Mousepad', + incomingCapabilities: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.request', + 'kdeconnect.mousepad.keyboardstate', + ], + outgoingCapabilities: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.request', + 'kdeconnect.mousepad.keyboardstate', + ], + actions: { + keyboard: { + label: _('Remote Input'), + icon_name: 'input-keyboard-symbolic', + + parameter_type: null, + incoming: [ + 'kdeconnect.mousepad.echo', + 'kdeconnect.mousepad.keyboardstate', + ], + outgoing: ['kdeconnect.mousepad.request'], + }, + }, +}; + +/** + * A map of "KDE Connect" keyvals to Gdk + */ +const KeyMap = new Map([ + [1, Gdk.KEY_BackSpace], + [2, Gdk.KEY_Tab], + [3, Gdk.KEY_Linefeed], + [4, Gdk.KEY_Left], + [5, Gdk.KEY_Up], + [6, Gdk.KEY_Right], + [7, Gdk.KEY_Down], + [8, Gdk.KEY_Page_Up], + [9, Gdk.KEY_Page_Down], + [10, Gdk.KEY_Home], + [11, Gdk.KEY_End], + [12, Gdk.KEY_Return], + [13, Gdk.KEY_Delete], + [14, Gdk.KEY_Escape], + [15, Gdk.KEY_Sys_Req], + [16, Gdk.KEY_Scroll_Lock], + [17, 0], + [18, 0], + [19, 0], + [20, 0], + [21, Gdk.KEY_F1], + [22, Gdk.KEY_F2], + [23, Gdk.KEY_F3], + [24, Gdk.KEY_F4], + [25, Gdk.KEY_F5], + [26, Gdk.KEY_F6], + [27, Gdk.KEY_F7], + [28, Gdk.KEY_F8], + [29, Gdk.KEY_F9], + [30, Gdk.KEY_F10], + [31, Gdk.KEY_F11], + [32, Gdk.KEY_F12], +]); + +const KeyMapCodes = new Map([ + [1, 14], + [2, 15], + [3, 101], + [4, 105], + [5, 103], + [6, 106], + [7, 108], + [8, 104], + [9, 109], + [10, 102], + [11, 107], + [12, 28], + [13, 111], + [14, 1], + [15, 99], + [16, 70], + [17, 0], + [18, 0], + [19, 0], + [20, 0], + [21, 59], + [22, 60], + [23, 61], + [24, 62], + [25, 63], + [26, 64], + [27, 65], + [28, 66], + [29, 67], + [30, 68], + [31, 87], + [32, 88], +]); + +/** + * Mousepad Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mousepad + * + * TODO: support outgoing mouse events? + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectMousepadPlugin', + Properties: { + 'state': GObject.ParamSpec.boolean( + 'state', + 'State', + 'Remote keyboard state', + GObject.ParamFlags.READABLE, + false + ), + }, +}, class Plugin extends PluginBase.Plugin { + _init(device) { + super._init(device, 'mousepad'); + + if (!globalThis.HAVE_GNOME) + this._input = Components.acquire('ydotool'); + else + this._input = Components.acquire('input'); + + this._shareControlChangedId = this.settings.connect( + 'changed::share-control', + this._sendState.bind(this) + ); + } + + get state() { + if (this._state === undefined) + this._state = false; + + return this._state; + } + + connected() { + super.connected(); + + this._sendState(); + } + + disconnected() { + super.disconnected(); + + this._state = false; + this.notify('state'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.mousepad.request': + this._handleInput(packet.body); + break; + + case 'kdeconnect.mousepad.echo': + this._handleEcho(packet.body); + break; + + case 'kdeconnect.mousepad.keyboardstate': + this._handleState(packet); + break; + } + } + + /** + * Handle a input event. + * + * @param {Object} input - The body of a `kdeconnect.mousepad.request` + */ + _handleInput(input) { + if (!this.settings.get_boolean('share-control')) + return; + + let keysym; + let modifiers = 0; + const modifiers_codes = []; + + // These are ordered, as much as possible, to create the shortest code + // path for high-frequency, low-latency events (eg. mouse movement) + switch (true) { + case input.hasOwnProperty('scroll'): + this._input.scrollPointer(input.dx, input.dy); + break; + + case (input.hasOwnProperty('dx') && input.hasOwnProperty('dy')): + this._input.movePointer(input.dx, input.dy); + break; + + case (input.hasOwnProperty('key') || input.hasOwnProperty('specialKey')): + // NOTE: \u0000 sometimes sent in advance of a specialKey packet + if (input.key && input.key === '\u0000') + return; + + // Modifiers + if (input.alt) { + modifiers |= Gdk.ModifierType.MOD1_MASK; + modifiers_codes.push(56); + } + + if (input.ctrl) { + modifiers |= Gdk.ModifierType.CONTROL_MASK; + modifiers_codes.push(29); + } + + if (input.shift) { + modifiers |= Gdk.ModifierType.SHIFT_MASK; + modifiers_codes.push(42); + } + + if (input.super) { + modifiers |= Gdk.ModifierType.SUPER_MASK; + modifiers_codes.push(125); + } + + // Regular key (printable ASCII or Unicode) + if (input.key) { + if (!globalThis.HAVE_GNOME) + this._input.pressKeys(input.key, modifiers_codes); + else + this._input.pressKeys(input.key, modifiers); + + this._sendEcho(input); + + // Special key (eg. non-printable ASCII) + } else if (input.specialKey && KeyMap.has(input.specialKey)) { + if (!globalThis.HAVE_GNOME) { + keysym = KeyMapCodes.get(input.specialKey); + this._input.pressKeys(keysym, modifiers_codes); + } else { + keysym = KeyMap.get(input.specialKey); + this._input.pressKeys(keysym, modifiers); + } + + this._sendEcho(input); + } + break; + + case input.hasOwnProperty('singleclick'): + this._input.clickPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('doubleclick'): + this._input.doubleclickPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('middleclick'): + this._input.clickPointer(Gdk.BUTTON_MIDDLE); + break; + + case input.hasOwnProperty('rightclick'): + this._input.clickPointer(Gdk.BUTTON_SECONDARY); + break; + + case input.hasOwnProperty('singlehold'): + this._input.pressPointer(Gdk.BUTTON_PRIMARY); + break; + + case input.hasOwnProperty('singlerelease'): + this._input.releasePointer(Gdk.BUTTON_PRIMARY); + break; + + default: + logError(new Error('Unknown input')); + } + } + + /** + * Handle an echo/ACK of a event we sent, displaying it the dialog entry. + * + * @param {Object} input - The body of a `kdeconnect.mousepad.echo` + */ + _handleEcho(input) { + if (!this._dialog || !this._dialog.visible) + return; + + // Skip modifiers + if (input.alt || input.ctrl || input.super) + return; + + if (input.key) { + this._dialog._isAck = true; + this._dialog.entry.buffer.text += input.key; + this._dialog._isAck = false; + } else if (KeyMap.get(input.specialKey) === Gdk.KEY_BackSpace) { + this._dialog.entry.emit('backspace'); + } + } + + /** + * Handle a state change from the remote keyboard. This is an indication + * that the remote keyboard is ready to accept input. + * + * @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet + */ + _handleState(packet) { + this._state = !!packet.body.state; + this.notify('state'); + } + + /** + * Send an echo/ACK of @input, if requested + * + * @param {Object} input - The body of a 'kdeconnect.mousepad.request' + */ + _sendEcho(input) { + if (!input.sendAck) + return; + + delete input.sendAck; + input.isAck = true; + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.echo', + body: input, + }); + } + + /** + * Send the local keyboard state + * + * @param {boolean} state - Whether we're ready to accept input + */ + _sendState() { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.keyboardstate', + body: { + state: this.settings.get_boolean('share-control'), + }, + }); + } + + /** + * Open the Keyboard Input dialog + */ + keyboard() { + if (this._dialog === undefined) { + this._dialog = new InputDialog({ + device: this.device, + plugin: this, + }); + } + + this._dialog.present(); + } + + destroy() { + if (this._input !== undefined) { + if (!globalThis.HAVE_GNOME) + this._input = Components.release('ydotool'); + else + this._input = Components.release('input'); + } + + if (this._dialog !== undefined) + this._dialog.destroy(); + + this.settings.disconnect(this._shareControlChangedId); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js new file mode 100644 index 0000000..04651e1 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/mpris.js @@ -0,0 +1,906 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const Config = imports.config; +const DBus = imports.service.utils.dbus; +const MPRIS = imports.service.components.mpris; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('MPRIS'), + description: _('Bidirectional remote media playback control'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.MPRIS', + incomingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'], + outgoingCapabilities: ['kdeconnect.mpris', 'kdeconnect.mpris.request'], + actions: {}, +}; + + +/** + * MPRIS Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/mpriscontrol + * + * See also: + * https://specifications.freedesktop.org/mpris-spec/latest/ + * https://github.com/GNOME/gnome-shell/blob/master/js/ui/mpris.js + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'mpris'); + + this._players = new Map(); + this._transferring = new WeakSet(); + this._updating = new WeakSet(); + + this._mpris = Components.acquire('mpris'); + + this._playerAddedId = this._mpris.connect( + 'player-added', + this._sendPlayerList.bind(this) + ); + + this._playerRemovedId = this._mpris.connect( + 'player-removed', + this._sendPlayerList.bind(this) + ); + + this._playerChangedId = this._mpris.connect( + 'player-changed', + this._onPlayerChanged.bind(this) + ); + + this._playerSeekedId = this._mpris.connect( + 'player-seeked', + this._onPlayerSeeked.bind(this) + ); + } + + connected() { + super.connected(); + + this._requestPlayerList(); + this._sendPlayerList(); + } + + disconnected() { + super.disconnected(); + + for (const [identity, player] of this._players) { + this._players.delete(identity); + player.destroy(); + } + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.mpris': + this._handleUpdate(packet); + break; + + case 'kdeconnect.mpris.request': + this._handleRequest(packet); + break; + } + } + + /** + * Handle a remote player update. + * + * @param {Core.Packet} packet - A `kdeconnect.mpris` + */ + _handleUpdate(packet) { + try { + if (packet.body.hasOwnProperty('playerList')) + this._handlePlayerList(packet.body.playerList); + else if (packet.body.hasOwnProperty('player')) + this._handlePlayerUpdate(packet); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Handle an updated list of remote players. + * + * @param {string[]} playerList - A list of remote player names + */ + _handlePlayerList(playerList) { + // Destroy removed players before adding new ones + for (const player of this._players.values()) { + if (!playerList.includes(player.Identity)) { + this._players.delete(player.Identity); + player.destroy(); + } + } + + for (const identity of playerList) { + if (!this._players.has(identity)) { + const player = new PlayerRemote(this.device, identity); + this._players.set(identity, player); + } + + // Always request player updates; packets are cheap + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: identity, + requestNowPlaying: true, + requestVolume: true, + }, + }); + } + } + + /** + * Handle an update for a remote player. + * + * @param {Object} packet - A `kdeconnect.mpris` packet + */ + _handlePlayerUpdate(packet) { + const player = this._players.get(packet.body.player); + + if (player === undefined) + return; + + if (packet.body.hasOwnProperty('transferringAlbumArt')) + player.handleAlbumArt(packet); + else + player.update(packet.body); + } + + /** + * Request a list of remote players. + */ + _requestPlayerList() { + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + requestPlayerList: true, + }, + }); + } + + /** + * Handle a request for player information or action. + * + * @param {Core.Packet} packet - a `kdeconnect.mpris.request` + * @return {undefined} no return value + */ + _handleRequest(packet) { + // A request for the list of players + if (packet.body.hasOwnProperty('requestPlayerList')) + return this._sendPlayerList(); + + // A request for an unknown player; send the list of players + if (!this._mpris.hasPlayer(packet.body.player)) + return this._sendPlayerList(); + + // An album art request + if (packet.body.hasOwnProperty('albumArtUrl')) + return this._sendAlbumArt(packet); + + // A player command + this._handleCommand(packet); + } + + /** + * Handle an incoming player command or information request + * + * @param {Core.Packet} packet - A `kdeconnect.mpris.request` + */ + async _handleCommand(packet) { + if (!this.settings.get_boolean('share-players')) + return; + + let player; + + try { + player = this._mpris.getPlayer(packet.body.player); + + if (player === undefined || this._updating.has(player)) + return; + + this._updating.add(player); + + // Player Actions + if (packet.body.hasOwnProperty('action')) { + switch (packet.body.action) { + case 'PlayPause': + case 'Play': + case 'Pause': + case 'Next': + case 'Previous': + case 'Stop': + player[packet.body.action](); + break; + + default: + debug(`unknown action: ${packet.body.action}`); + } + } + + // Player Properties + if (packet.body.hasOwnProperty('setLoopStatus')) + player.LoopStatus = packet.body.setLoopStatus; + + if (packet.body.hasOwnProperty('setShuffle')) + player.Shuffle = packet.body.setShuffle; + + if (packet.body.hasOwnProperty('setVolume')) + player.Volume = packet.body.setVolume / 100; + + if (packet.body.hasOwnProperty('Seek')) + await player.Seek(packet.body.Seek * 1000); + + if (packet.body.hasOwnProperty('SetPosition')) { + const offset = (packet.body.SetPosition * 1000) - player.Position; + await player.Seek(offset); + } + + // Information Request + let hasResponse = false; + + const response = { + type: 'kdeconnect.mpris', + body: { + player: packet.body.player, + }, + }; + + if (packet.body.hasOwnProperty('requestNowPlaying')) { + hasResponse = true; + + Object.assign(response.body, { + pos: Math.floor(player.Position / 1000), + isPlaying: (player.PlaybackStatus === 'Playing'), + canPause: player.CanPause, + canPlay: player.CanPlay, + canGoNext: player.CanGoNext, + canGoPrevious: player.CanGoPrevious, + canSeek: player.CanSeek, + loopStatus: player.LoopStatus, + shuffle: player.Shuffle, + + // default values for members that will be filled conditionally + albumArtUrl: '', + length: 0, + artist: '', + title: '', + album: '', + nowPlaying: '', + volume: 0, + }); + + const metadata = player.Metadata; + + if (metadata.hasOwnProperty('mpris:artUrl')) { + const file = Gio.File.new_for_uri(metadata['mpris:artUrl']); + response.body.albumArtUrl = file.get_uri(); + } + + if (metadata.hasOwnProperty('mpris:length')) { + const trackLen = Math.floor(metadata['mpris:length'] / 1000); + response.body.length = trackLen; + } + + if (metadata.hasOwnProperty('xesam:artist')) { + const artists = metadata['xesam:artist']; + response.body.artist = artists.join(', '); + } + + if (metadata.hasOwnProperty('xesam:title')) + response.body.title = metadata['xesam:title']; + + if (metadata.hasOwnProperty('xesam:album')) + response.body.album = metadata['xesam:album']; + + // Now Playing + if (response.body.artist && response.body.title) { + response.body.nowPlaying = [ + response.body.artist, + response.body.title, + ].join(' - '); + } else if (response.body.artist) { + response.body.nowPlaying = response.body.artist; + } else if (response.body.title) { + response.body.nowPlaying = response.body.title; + } else { + response.body.nowPlaying = _('Unknown'); + } + } + + if (packet.body.hasOwnProperty('requestVolume')) { + hasResponse = true; + response.body.volume = Math.floor(player.Volume * 100); + } + + if (hasResponse) + this.device.sendPacket(response); + } catch (e) { + debug(e, this.device.name); + } finally { + this._updating.delete(player); + } + } + + _onPlayerChanged(mpris, player) { + if (!this.settings.get_boolean('share-players')) + return; + + this._handleCommand({ + body: { + player: player.Identity, + requestNowPlaying: true, + requestVolume: true, + }, + }); + } + + _onPlayerSeeked(mpris, player, offset) { + // TODO: although we can handle full seeked signals, kdeconnect-android + // does not, and expects a position update instead + this.device.sendPacket({ + type: 'kdeconnect.mpris', + body: { + player: player.Identity, + pos: Math.floor(player.Position / 1000), + // Seek: Math.floor(offset / 1000), + }, + }); + } + + async _sendAlbumArt(packet) { + let player; + + try { + // Reject concurrent requests for album art + player = this._mpris.getPlayer(packet.body.player); + + if (player === undefined || this._transferring.has(player)) + return; + + // Ensure the requested albumArtUrl matches the current mpris:artUrl + const metadata = player.Metadata; + + if (!metadata.hasOwnProperty('mpris:artUrl')) + return; + + const file = Gio.File.new_for_uri(metadata['mpris:artUrl']); + const request = Gio.File.new_for_uri(packet.body.albumArtUrl); + + if (file.get_uri() !== request.get_uri()) + throw RangeError(`invalid URI "${packet.body.albumArtUrl}"`); + + // Transfer the album art + this._transferring.add(player); + + const transfer = this.device.createTransfer(); + + transfer.addFile({ + type: 'kdeconnect.mpris', + body: { + transferringAlbumArt: true, + player: packet.body.player, + albumArtUrl: packet.body.albumArtUrl, + }, + }, file); + + await transfer.start(); + } catch (e) { + debug(e, this.device.name); + } finally { + this._transferring.delete(player); + } + } + + /** + * Send the list of player identities and indicate whether we support + * transferring album art + */ + _sendPlayerList() { + let playerList = []; + + if (this.settings.get_boolean('share-players')) + playerList = this._mpris.getIdentities(); + + this.device.sendPacket({ + type: 'kdeconnect.mpris', + body: { + playerList: playerList, + supportAlbumArtPayload: true, + }, + }); + } + + destroy() { + if (this._mpris !== undefined) { + this._mpris.disconnect(this._playerAddedId); + this._mpris.disconnect(this._playerRemovedId); + this._mpris.disconnect(this._playerChangedId); + this._mpris.disconnect(this._playerSeekedId); + this._mpris = Components.release('mpris'); + } + + for (const [identity, player] of this._players) { + this._players.delete(identity); + player.destroy(); + } + + super.destroy(); + } +}); + + +/* + * A class for mirroring a remote Media Player on DBus + */ +const PlayerRemote = GObject.registerClass({ + GTypeName: 'GSConnectMPRISPlayerRemote', +}, class PlayerRemote extends MPRIS.Player { + + _init(device, identity) { + super._init(); + + this._device = device; + this._Identity = identity; + this._isPlaying = false; + + this._artist = null; + this._title = null; + this._album = null; + this._length = 0; + this._artUrl = null; + + this._ownerId = 0; + this._connection = null; + this._applicationIface = null; + this._playerIface = null; + } + + _getFile(albumArtUrl) { + const hash = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5, + albumArtUrl, -1); + const path = GLib.build_filenamev([Config.CACHEDIR, hash]); + + return Gio.File.new_for_uri(`file://${path}`); + } + + _requestAlbumArt(state) { + if (this._artUrl === state.albumArtUrl) + return; + + const file = this._getFile(state.albumArtUrl); + + if (file.query_exists(null)) { + this._artUrl = file.get_uri(); + this._Metadata = undefined; + this.notify('Metadata'); + } else { + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + albumArtUrl: state.albumArtUrl, + }, + }); + } + } + + _updateMetadata(state) { + let metadataChanged = false; + + if (state.hasOwnProperty('artist')) { + if (this._artist !== state.artist) { + this._artist = state.artist; + metadataChanged = true; + } + } else if (this._artist) { + this._artist = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('title')) { + if (this._title !== state.title) { + this._title = state.title; + metadataChanged = true; + } + } else if (this._title) { + this._title = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('album')) { + if (this._album !== state.album) { + this._album = state.album; + metadataChanged = true; + } + } else if (this._album) { + this._album = null; + metadataChanged = true; + } + + if (state.hasOwnProperty('length')) { + if (this._length !== state.length * 1000) { + this._length = state.length * 1000; + metadataChanged = true; + } + } else if (this._length) { + this._length = 0; + metadataChanged = true; + } + + if (state.hasOwnProperty('albumArtUrl')) { + this._requestAlbumArt(state); + } else if (this._artUrl) { + this._artUrl = null; + metadataChanged = true; + } + + if (metadataChanged) { + this._Metadata = undefined; + this.notify('Metadata'); + } + } + + async export() { + try { + if (this._connection === null) { + this._connection = await DBus.newConnection(); + const MPRISIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2'); + const MPRISPlayerIface = Config.DBUS.lookup_interface('org.mpris.MediaPlayer2.Player'); + + if (this._applicationIface === null) { + this._applicationIface = new DBus.Interface({ + g_instance: this, + g_connection: this._connection, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_info: MPRISIface, + }); + } + + if (this._playerIface === null) { + this._playerIface = new DBus.Interface({ + g_instance: this, + g_connection: this._connection, + g_object_path: '/org/mpris/MediaPlayer2', + g_interface_info: MPRISPlayerIface, + }); + } + } + + if (this._ownerId !== 0) + return; + + const name = [ + this.device.name, + this.Identity, + ].join('').replace(/[\W]*/g, ''); + + this._ownerId = Gio.bus_own_name_on_connection( + this._connection, + `org.mpris.MediaPlayer2.GSConnect.${name}`, + Gio.BusNameOwnerFlags.NONE, + null, + null + ); + } catch (e) { + debug(e, this.Identity); + } + } + + unexport() { + if (this._ownerId === 0) + return; + + Gio.bus_unown_name(this._ownerId); + this._ownerId = 0; + } + + /** + * Download album art for the current track of the remote player. + * + * @param {Core.Packet} packet - A `kdeconnect.mpris` packet + */ + async handleAlbumArt(packet) { + let file; + + try { + file = this._getFile(packet.body.albumArtUrl); + + // Transfer the album art + const transfer = this.device.createTransfer(); + transfer.addFile(packet, file); + + await transfer.start(); + + this._artUrl = file.get_uri(); + this._Metadata = undefined; + this.notify('Metadata'); + } catch (e) { + debug(e, this.device.name); + + if (file) + file.delete_async(GLib.PRIORITY_DEFAULT, null, null); + } + } + + /** + * Update the internal state of the media player. + * + * @param {Core.Packet} state - The body of a `kdeconnect.mpris` packet + */ + update(state) { + this.freeze_notify(); + + // Metadata + if (state.hasOwnProperty('nowPlaying') || + state.hasOwnProperty('artist') || + state.hasOwnProperty('title')) + this._updateMetadata(state); + + // Playback Status + if (state.hasOwnProperty('isPlaying')) { + if (this._isPlaying !== state.isPlaying) { + this._isPlaying = state.isPlaying; + this.notify('PlaybackStatus'); + } + } + + if (state.hasOwnProperty('canPlay')) { + if (this.CanPlay !== state.canPlay) { + this._CanPlay = state.canPlay; + this.notify('CanPlay'); + } + } + + if (state.hasOwnProperty('canPause')) { + if (this.CanPause !== state.canPause) { + this._CanPause = state.canPause; + this.notify('CanPause'); + } + } + + if (state.hasOwnProperty('canGoNext')) { + if (this.CanGoNext !== state.canGoNext) { + this._CanGoNext = state.canGoNext; + this.notify('CanGoNext'); + } + } + + if (state.hasOwnProperty('canGoPrevious')) { + if (this.CanGoPrevious !== state.canGoPrevious) { + this._CanGoPrevious = state.canGoPrevious; + this.notify('CanGoPrevious'); + } + } + + if (state.hasOwnProperty('pos')) + this._Position = state.pos * 1000; + + if (state.hasOwnProperty('volume')) { + if (this.Volume !== state.volume / 100) { + this._Volume = state.volume / 100; + this.notify('Volume'); + } + } + + this.thaw_notify(); + + if (!this._isPlaying && !this.CanControl) + this.unexport(); + else + this.export(); + } + + /* + * Native properties + */ + get device() { + return this._device; + } + + /* + * The org.mpris.MediaPlayer2.Player Interface + */ + get CanControl() { + return (this.CanPlay || this.CanPause); + } + + get Metadata() { + if (this._Metadata === undefined) { + this._Metadata = {}; + + if (this._artist) { + this._Metadata['xesam:artist'] = new GLib.Variant('as', + [this._artist]); + } + + if (this._title) { + this._Metadata['xesam:title'] = new GLib.Variant('s', + this._title); + } + + if (this._album) { + this._Metadata['xesam:album'] = new GLib.Variant('s', + this._album); + } + + if (this._artUrl) { + this._Metadata['mpris:artUrl'] = new GLib.Variant('s', + this._artUrl); + } + + this._Metadata['mpris:length'] = new GLib.Variant('x', + this._length); + } + + return this._Metadata; + } + + get PlaybackStatus() { + if (this._isPlaying) + return 'Playing'; + + return 'Stopped'; + } + + get Volume() { + if (this._Volume === undefined) + this._Volume = 0.3; + + return this._Volume; + } + + set Volume(level) { + if (this._Volume === level) + return; + + this._Volume = level; + this.notify('Volume'); + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + setVolume: Math.floor(this._Volume * 100), + }, + }); + } + + Next() { + if (!this.CanGoNext) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Next', + }, + }); + } + + Pause() { + if (!this.CanPause) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Pause', + }, + }); + } + + Play() { + if (!this.CanPlay) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Play', + }, + }); + } + + PlayPause() { + if (!this.CanPlay && !this.CanPause) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'PlayPause', + }, + }); + } + + Previous() { + if (!this.CanGoPrevious) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Previous', + }, + }); + } + + Seek(offset) { + if (!this.CanSeek) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + Seek: offset, + }, + }); + } + + SetPosition(trackId, position) { + debug(`${this._Identity}: SetPosition(${trackId}, ${position})`); + + if (!this.CanControl || !this.CanSeek) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + SetPosition: position / 1000, + }, + }); + } + + Stop() { + if (!this.CanControl) + return; + + this.device.sendPacket({ + type: 'kdeconnect.mpris.request', + body: { + player: this.Identity, + action: 'Stop', + }, + }); + } + + destroy() { + this.unexport(); + + if (this._connection) { + this._connection.close(null, null); + this._connection = null; + + if (this._applicationIface) { + this._applicationIface.destroy(); + this._applicationIface = null; + } + + if (this._playerIface) { + this._playerIface.destroy(); + this._playerIface = null; + } + } + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js new file mode 100644 index 0000000..3697615 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/notification.js @@ -0,0 +1,694 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Components = imports.service.components; +const Config = imports.config; +const PluginBase = imports.service.plugin; +const NotificationUI = imports.service.ui.notification; + + +var Metadata = { + label: _('Notifications'), + description: _('Share notifications with the paired device'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Notification', + incomingCapabilities: [ + 'kdeconnect.notification', + 'kdeconnect.notification.request', + ], + outgoingCapabilities: [ + 'kdeconnect.notification', + 'kdeconnect.notification.action', + 'kdeconnect.notification.reply', + 'kdeconnect.notification.request', + ], + actions: { + withdrawNotification: { + label: _('Cancel Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.notification'], + }, + closeNotification: { + label: _('Close Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.notification.request'], + }, + replyNotification: { + label: _('Reply Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('(ssa{ss})'), + incoming: ['kdeconnect.notification'], + outgoing: ['kdeconnect.notification.reply'], + }, + sendNotification: { + label: _('Send Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('a{sv}'), + incoming: [], + outgoing: ['kdeconnect.notification'], + }, + activateNotification: { + label: _('Activate Notification'), + icon_name: 'preferences-system-notifications-symbolic', + + parameter_type: new GLib.VariantType('(ss)'), + incoming: [], + outgoing: ['kdeconnect.notification.action'], + }, + }, +}; + + +// A regex for our custom notificaiton ids +const ID_REGEX = /^(fdo|gtk)\|([^|]+)\|(.*)$/; + +// A list of known SMS apps +const SMS_APPS = [ + // Popular apps that don't contain the string 'sms' + 'com.android.messaging', // AOSP + 'com.google.android.apps.messaging', // Google Messages + 'com.textra', // Textra + 'xyz.klinker.messenger', // Pulse + 'com.calea.echo', // Mood Messenger + 'com.moez.QKSMS', // QKSMS + 'rpkandrodev.yaata', // YAATA + 'com.tencent.mm', // WeChat + 'com.viber.voip', // Viber + 'com.kakao.talk', // KakaoTalk + 'com.concentriclivers.mms.com.android.mms', // AOSP Clone + 'fr.slvn.mms', // AOSP Clone + 'com.promessage.message', // + 'com.htc.sense.mms', // HTC Messages + + // Known not to work with sms plugin + 'org.thoughtcrime.securesms', // Signal Private Messenger + 'com.samsung.android.messaging', // Samsung Messages +]; + + +/** + * Try to determine if an notification is from an SMS app + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @return {boolean} Whether the notification is from an SMS app + */ +function _isSmsNotification(packet) { + const id = packet.body.id; + + if (id.includes('sms')) + return true; + + for (let i = 0, len = SMS_APPS.length; i < len; i++) { + if (id.includes(SMS_APPS[i])) + return true; + } + + return false; +} + + +/** + * Remove a local libnotify or Gtk notification. + * + * @param {String|Number} id - Gtk (string) or libnotify id (uint32) + * @param {String|null} application - Application Id if Gtk or null + */ +function _removeNotification(id, application = null) { + let name, path, method, variant; + + if (application !== null) { + name = 'org.gtk.Notifications'; + method = 'RemoveNotification'; + path = '/org/gtk/Notifications'; + variant = new GLib.Variant('(ss)', [application, id]); + } else { + name = 'org.freedesktop.Notifications'; + path = '/org/freedesktop/Notifications'; + method = 'CloseNotification'; + variant = new GLib.Variant('(u)', [id]); + } + + Gio.DBus.session.call( + name, path, name, method, variant, null, + Gio.DBusCallFlags.NONE, -1, null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + logError(e); + } + } + ); +} + + +/** + * Notification Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/notifications + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sendnotifications + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectNotificationPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'notification'); + + this._listener = Components.acquire('notification'); + this._session = Components.acquire('session'); + + this._notificationAddedId = this._listener.connect( + 'notification-added', + this._onNotificationAdded.bind(this) + ); + + // Load application notification settings + this._applicationsChangedId = this.settings.connect( + 'changed::applications', + this._onApplicationsChanged.bind(this) + ); + this._onApplicationsChanged(this.settings, 'applications'); + this._applicationsChangedSkip = false; + } + + connected() { + super.connected(); + + this._requestNotifications(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.notification': + this._handleNotification(packet); + break; + + // TODO + case 'kdeconnect.notification.action': + this._handleNotificationAction(packet); + break; + + // No Linux/BSD desktop notifications are repliable as yet + case 'kdeconnect.notification.reply': + debug(`Not implemented: ${packet.type}`); + break; + + case 'kdeconnect.notification.request': + this._handleNotificationRequest(packet); + break; + + default: + debug(`Unknown notification packet: ${packet.type}`); + } + } + + _onApplicationsChanged(settings, key) { + if (this._applicationsChangedSkip) + return; + + try { + const json = settings.get_string(key); + this._applications = JSON.parse(json); + } catch (e) { + debug(e, this.device.name); + + this._applicationsChangedSkip = true; + settings.set_string(key, '{}'); + this._applicationsChangedSkip = false; + } + } + + _onNotificationAdded(listener, notification) { + try { + const notif = notification.full_unpack(); + + // An unconfigured application + if (notif.appName && !this._applications[notif.appName]) { + this._applications[notif.appName] = { + iconName: 'system-run-symbolic', + enabled: true, + }; + + // Store the themed icons for the device preferences window + if (notif.icon === undefined) { + // Keep default + + } else if (typeof notif.icon === 'string') { + this._applications[notif.appName].iconName = notif.icon; + + } else if (notif.icon instanceof Gio.ThemedIcon) { + const iconName = notif.icon.get_names()[0]; + this._applications[notif.appName].iconName = iconName; + } + + this._applicationsChangedSkip = true; + this.settings.set_string( + 'applications', + JSON.stringify(this._applications) + ); + this._applicationsChangedSkip = false; + } + + // Sending notifications forbidden + if (!this.settings.get_boolean('send-notifications')) + return; + + // Sending when the session is active is forbidden + if (!this.settings.get_boolean('send-active') && this._session.active) + return; + + // Notifications disabled for this application + if (notif.appName && !this._applications[notif.appName].enabled) + return; + + this.sendNotification(notif); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Handle an incoming notification or closed report. + * + * FIXME: upstream kdeconnect-android is tagging many notifications as + * `silent`, causing them to never be shown. Since we already handle + * duplicates in the Shell, we ignore that flag for now. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + */ + _handleNotification(packet) { + // A report that a remote notification has been dismissed + if (packet.body.hasOwnProperty('isCancel')) + this.device.hideNotification(packet.body.id); + + // A normal, remote notification + else + this._receiveNotification(packet); + } + + /** + * Handle an incoming request to activate a notification action. + * + * @param {Core.Packet} packet - A `kdeconnect.notification.action` + */ + _handleNotificationAction(packet) { + throw new GObject.NotImplementedError(); + } + + /** + * Handle an incoming request to close or list notifications. + * + * @param {Core.Packet} packet - A `kdeconnect.notification.request` + */ + _handleNotificationRequest(packet) { + // A request for our notifications. This isn't implemented and would be + // pretty hard to without communicating with GNOME Shell. + if (packet.body.hasOwnProperty('request')) + return; + + // A request to close a local notification + // + // TODO: kdeconnect-android doesn't send these, and will instead send a + // kdeconnect.notification packet with isCancel and an id of "0". + // + // For clients that do support it, we report notification ids in the + // form "type|application-id|notification-id" so we can close it with + // the appropriate service. + if (packet.body.hasOwnProperty('cancel')) { + const [, type, application, id] = ID_REGEX.exec(packet.body.cancel); + + if (type === 'fdo') + _removeNotification(parseInt(id)); + else if (type === 'gtk') + _removeNotification(id, application); + } + } + + /** + * Upload an icon from a GLib.Bytes object. + * + * @param {Core.Packet} packet - The packet for the notification + * @param {GLib.Bytes} bytes - The icon bytes + */ + _uploadBytesIcon(packet, bytes) { + const stream = Gio.MemoryInputStream.new_from_bytes(bytes); + this._uploadIconStream(packet, stream, bytes.get_size()); + } + + /** + * Upload an icon from a Gio.File object. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @param {Gio.File} file - A file object for the icon + */ + async _uploadFileIcon(packet, file) { + const read = file.read_async(GLib.PRIORITY_DEFAULT, null); + const query = file.query_info_async('standard::size', + Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null); + const [stream, info] = await Promise.all([read, query]); + + this._uploadIconStream(packet, stream, info.get_size()); + } + + /** + * A function for uploading GThemedIcons + * + * @param {Core.Packet} packet - The packet for the notification + * @param {Gio.ThemedIcon} icon - The GIcon to upload + */ + _uploadThemedIcon(packet, icon) { + const theme = Gtk.IconTheme.get_default(); + let file = null; + + for (const name of icon.names) { + // NOTE: kdeconnect-android doesn't support SVGs + const size = Math.max.apply(null, theme.get_icon_sizes(name)); + const info = theme.lookup_icon(name, size, Gtk.IconLookupFlags.NO_SVG); + + // Send the first icon we find from the options + if (info) { + file = Gio.File.new_for_path(info.get_filename()); + break; + } + } + + if (file) + this._uploadFileIcon(packet, file); + else + this.device.sendPacket(packet); + } + + /** + * All icon types end up being uploaded in this function. + * + * @param {Core.Packet} packet - The packet for the notification + * @param {Gio.InputStream} stream - A stream to read the icon bytes from + * @param {number} size - Size of the icon in bytes + */ + async _uploadIconStream(packet, stream, size) { + try { + const transfer = this.device.createTransfer(); + transfer.addStream(packet, stream, size); + + await transfer.start(); + } catch (e) { + debug(e); + + this.device.sendPacket(packet); + } + } + + /** + * Upload an icon from a GIcon or themed icon name. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + * @param {Gio.Icon|string|null} icon - An icon or %null + * @return {Promise} A promise for the operation + */ + _uploadIcon(packet, icon = null) { + // Normalize strings into GIcons + if (typeof icon === 'string') + icon = Gio.Icon.new_for_string(icon); + + if (icon instanceof Gio.ThemedIcon) + return this._uploadThemedIcon(packet, icon); + + if (icon instanceof Gio.FileIcon) + return this._uploadFileIcon(packet, icon.get_file()); + + if (icon instanceof Gio.BytesIcon) + return this._uploadBytesIcon(packet, icon.get_bytes()); + + return this.device.sendPacket(packet); + } + + /** + * Send a local notification to the remote device. + * + * @param {Object} notif - A dictionary of notification parameters + * @param {string} notif.appName - The notifying application + * @param {string} notif.id - The notification ID + * @param {string} notif.title - The notification title + * @param {string} notif.body - The notification body + * @param {string} notif.ticker - The notification title & body + * @param {boolean} notif.isClearable - If the notification can be closed + * @param {string|Gio.Icon} notif.icon - An icon name or GIcon + */ + async sendNotification(notif) { + try { + const icon = notif.icon || null; + delete notif.icon; + + await this._uploadIcon({ + type: 'kdeconnect.notification', + body: notif, + }, icon); + } catch (e) { + logError(e); + } + } + + async _downloadIcon(packet) { + try { + if (!packet.hasPayload()) + return null; + + // Save the file in the global cache + const path = GLib.build_filenamev([ + Config.CACHEDIR, + packet.body.payloadHash || `${Date.now()}`, + ]); + + // Check if we've already downloaded this icon + // NOTE: if we reject the transfer kdeconnect-android will resend + // the notification packet, which may cause problems wrt #789 + const file = Gio.File.new_for_path(path); + + if (file.query_exists(null)) + return new Gio.FileIcon({file: file}); + + // Open the target path and create a transfer + const transfer = this.device.createTransfer(); + + transfer.addFile(packet, file); + + try { + await transfer.start(); + + return new Gio.FileIcon({file: file}); + } catch (e) { + debug(e, this.device.name); + + file.delete_async(GLib.PRIORITY_DEFAULT, null, null); + return null; + } + } catch (e) { + debug(e, this.device.name); + return null; + } + } + + /** + * Receive an incoming notification. + * + * @param {Core.Packet} packet - A `kdeconnect.notification` + */ + async _receiveNotification(packet) { + try { + // Set defaults + let action = null; + let buttons = []; + let id = packet.body.id; + let title = packet.body.appName; + let body = `${packet.body.title}: ${packet.body.text}`; + let icon = await this._downloadIcon(packet); + + // Repliable Notification + if (packet.body.requestReplyId) { + id = `${packet.body.id}|${packet.body.requestReplyId}`; + action = { + name: 'replyNotification', + parameter: new GLib.Variant('(ssa{ss})', [ + packet.body.requestReplyId, + '', + { + appName: packet.body.appName, + title: packet.body.title, + text: packet.body.text, + }, + ]), + }; + } + + // Notification Actions + if (packet.body.actions) { + buttons = packet.body.actions.map(action => { + return { + label: action, + action: 'activateNotification', + parameter: new GLib.Variant('(ss)', [id, action]), + }; + }); + } + + // Special case for Missed Calls + if (packet.body.id.includes('MissedCall')) { + title = packet.body.title; + body = packet.body.text; + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'call-missed-symbolic'}); + + // Special case for SMS notifications + } else if (_isSmsNotification(packet)) { + title = packet.body.title; + body = packet.body.text; + action = { + name: 'replySms', + parameter: new GLib.Variant('s', packet.body.title), + }; + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'sms-symbolic'}); + + // Special case where 'appName' is the same as 'title' + } else if (packet.body.appName === packet.body.title) { + body = packet.body.text; + } + + // Use the device icon if we still don't have one + if (icon === null) + icon = new Gio.ThemedIcon({name: this.device.icon_name}); + + // Show the notification + this.device.showNotification({ + id: id, + title: title, + body: body, + icon: icon, + action: action, + buttons: buttons, + }); + } catch (e) { + logError(e); + } + } + + /** + * Request the remote notifications be sent + */ + _requestNotifications() { + this.device.sendPacket({ + type: 'kdeconnect.notification.request', + body: {request: true}, + }); + } + + /** + * Report that a local notification has been closed/dismissed. + * TODO: kdeconnect-android doesn't handle incoming isCancel packets. + * + * @param {string} id - The local notification id + */ + withdrawNotification(id) { + this.device.sendPacket({ + type: 'kdeconnect.notification', + body: { + isCancel: true, + id: id, + }, + }); + } + + /** + * Close a remote notification. + * TODO: ignore local notifications + * + * @param {string} id - The remote notification id + */ + closeNotification(id) { + this.device.sendPacket({ + type: 'kdeconnect.notification.request', + body: {cancel: id}, + }); + } + + /** + * Reply to a notification sent with a requestReplyId UUID + * + * @param {string} uuid - The requestReplyId for the repliable notification + * @param {string} message - The message to reply with + * @param {Object} notification - The original notification packet + */ + replyNotification(uuid, message, notification) { + // If this happens for some reason, things will explode + if (!uuid) + throw Error('Missing UUID'); + + // If the message has no content, open a dialog for the user to add one + if (!message) { + const dialog = new NotificationUI.ReplyDialog({ + device: this.device, + uuid: uuid, + notification: notification, + plugin: this, + }); + dialog.present(); + + // Otherwise just send the reply + } else { + this.device.sendPacket({ + type: 'kdeconnect.notification.reply', + body: { + requestReplyId: uuid, + message: message, + }, + }); + } + } + + /** + * Activate a remote notification action + * + * @param {string} id - The remote notification id + * @param {string} action - The notification action (label) + */ + activateNotification(id, action) { + this.device.sendPacket({ + type: 'kdeconnect.notification.action', + body: { + action: action, + key: id, + }, + }); + } + + destroy() { + this.settings.disconnect(this._applicationsChangedId); + + if (this._listener !== undefined) { + this._listener.disconnect(this._notificationAddedId); + this._listener = Components.release('notification'); + } + + if (this._session !== undefined) + this._session = Components.release('session'); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js new file mode 100644 index 0000000..3eb194b --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/ping.js @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Ping'), + description: _('Send and receive pings'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Ping', + incomingCapabilities: ['kdeconnect.ping'], + outgoingCapabilities: ['kdeconnect.ping'], + actions: { + ping: { + label: _('Ping'), + icon_name: 'dialog-information-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.ping'], + }, + }, +}; + + +/** + * Ping Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/ping + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPingPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'ping'); + } + + handlePacket(packet) { + // Notification + const notif = { + title: this.device.name, + body: _('Ping'), + icon: new Gio.ThemedIcon({name: `${this.device.icon_name}`}), + }; + + if (packet.body.message) { + // TRANSLATORS: An optional message accompanying a ping, rarely if ever used + // eg. Ping: A message sent with ping + notif.body = _('Ping: %s').format(packet.body.message); + } + + this.device.showNotification(notif); + } + + ping(message = '') { + const packet = { + type: 'kdeconnect.ping', + body: {}, + }; + + if (message.length) + packet.body.message = message; + + this.device.sendPacket(packet); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js new file mode 100644 index 0000000..7d9cf94 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/presenter.js @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Presentation'), + description: _('Use the paired device as a presenter'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Presenter', + incomingCapabilities: ['kdeconnect.presenter'], + outgoingCapabilities: [], + actions: {}, +}; + + +/** + * Presenter Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/presenter + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/PresenterPlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectPresenterPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'presenter'); + + if (!globalThis.HAVE_GNOME) + this._input = Components.acquire('ydotool'); + else + this._input = Components.acquire('input'); + } + + handlePacket(packet) { + if (packet.body.hasOwnProperty('dx')) { + this._input.movePointer( + packet.body.dx * 1000, + packet.body.dy * 1000 + ); + } else if (packet.body.stop) { + // Currently unsupported and unnecessary as we just re-use the mouse + // pointer instead of showing an arbitrary window. + } + } + + destroy() { + if (this._input !== undefined) { + if (!globalThis.HAVE_GNOME) + this._input = Components.release('ydotool'); + else + this._input = Components.release('input'); + } + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js new file mode 100644 index 0000000..44f0005 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/runcommand.js @@ -0,0 +1,254 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Run Commands'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.RunCommand', + description: _('Run commands on your paired device or let the device run predefined commands on this PC'), + incomingCapabilities: [ + 'kdeconnect.runcommand', + 'kdeconnect.runcommand.request', + ], + outgoingCapabilities: [ + 'kdeconnect.runcommand', + 'kdeconnect.runcommand.request', + ], + actions: { + commands: { + label: _('Commands'), + icon_name: 'system-run-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: ['kdeconnect.runcommand'], + outgoing: ['kdeconnect.runcommand.request'], + }, + executeCommand: { + label: _('Commands'), + icon_name: 'system-run-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: ['kdeconnect.runcommand'], + outgoing: ['kdeconnect.runcommand.request'], + }, + }, +}; + + +/** + * RunCommand Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/remotecommands + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/runcommand + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectRunCommandPlugin', + Properties: { + 'remote-commands': GObject.param_spec_variant( + 'remote-commands', + 'Remote Command List', + 'A list of the device\'s remote commands', + new GLib.VariantType('a{sv}'), + null, + GObject.ParamFlags.READABLE + ), + }, +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'runcommand'); + + // Local Commands + this._commandListChangedId = this.settings.connect( + 'changed::command-list', + this._sendCommandList.bind(this) + ); + + // We cache remote commands so they can be used in the settings even + // when the device is offline. + this._remote_commands = {}; + this.cacheProperties(['_remote_commands']); + } + + get remote_commands() { + return this._remote_commands; + } + + connected() { + super.connected(); + + this._sendCommandList(); + this._requestCommandList(); + this._handleCommandList(this.remote_commands); + } + + clearCache() { + this._remote_commands = {}; + this.notify('remote-commands'); + } + + cacheLoaded() { + if (!this.device.connected) + return; + + this._sendCommandList(); + this._requestCommandList(); + this._handleCommandList(this.remote_commands); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.runcommand': + this._handleCommandList(packet.body.commandList); + break; + + case 'kdeconnect.runcommand.request': + if (packet.body.hasOwnProperty('key')) + this._handleCommand(packet.body.key); + + else if (packet.body.hasOwnProperty('requestCommandList')) + this._sendCommandList(); + + break; + } + } + + /** + * Handle a request to execute the local command with the UUID @key + * + * @param {string} key - The UUID of the local command + */ + _handleCommand(key) { + try { + const commands = this.settings.get_value('command-list'); + const commandList = commands.recursiveUnpack(); + + if (!commandList.hasOwnProperty(key)) { + throw new Gio.IOErrorEnum({ + code: Gio.IOErrorEnum.PERMISSION_DENIED, + message: `Unknown command: ${key}`, + }); + } + + this.device.launchProcess([ + '/bin/sh', + '-c', + commandList[key].command, + ]); + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Parse the response to a request for the remote command list. Remove the + * command menu if there are no commands, otherwise amend the menu. + * + * @param {string|Object[]} commandList - A list of remote commands + */ + _handleCommandList(commandList) { + // See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051 + if (typeof commandList === 'string') { + try { + commandList = JSON.parse(commandList); + } catch (e) { + commandList = {}; + } + } + + this._remote_commands = commandList; + this.notify('remote-commands'); + + const commandEntries = Object.entries(this.remote_commands); + + // If there are no commands, hide the menu by disabling the action + this.device.lookup_action('commands').enabled = (commandEntries.length > 0); + + // Commands Submenu + const submenu = new Gio.Menu(); + + for (const [uuid, info] of commandEntries) { + const item = new Gio.MenuItem(); + item.set_label(info.name); + item.set_icon( + new Gio.ThemedIcon({name: 'application-x-executable-symbolic'}) + ); + item.set_detailed_action(`device.executeCommand::${uuid}`); + submenu.append_item(item); + } + + // Commands Item + const item = new Gio.MenuItem(); + item.set_detailed_action('device.commands::menu'); + item.set_attribute_value( + 'hidden-when', + new GLib.Variant('s', 'action-disabled') + ); + item.set_icon(new Gio.ThemedIcon({name: 'system-run-symbolic'})); + item.set_label(_('Commands')); + item.set_submenu(submenu); + + // If the submenu item is already present it will be replaced + const menuActions = this.device.settings.get_strv('menu-actions'); + const index = menuActions.indexOf('commands'); + + if (index > -1) { + this.device.removeMenuAction('device.commands'); + this.device.addMenuItem(item, index); + } + } + + /** + * Send a request for the remote command list + */ + _requestCommandList() { + this.device.sendPacket({ + type: 'kdeconnect.runcommand.request', + body: {requestCommandList: true}, + }); + } + + /** + * Send the local command list + */ + _sendCommandList() { + const commands = this.settings.get_value('command-list').recursiveUnpack(); + const commandList = JSON.stringify(commands); + + this.device.sendPacket({ + type: 'kdeconnect.runcommand', + body: {commandList: commandList}, + }); + } + + /** + * Placeholder function for command action + */ + commands() {} + + /** + * Send a request to execute the remote command with the UUID @key + * + * @param {string} key - The UUID of the remote command + */ + executeCommand(key) { + this.device.sendPacket({ + type: 'kdeconnect.runcommand.request', + body: {key: key}, + }); + } + + destroy() { + this.settings.disconnect(this._commandListChangedId); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js new file mode 100644 index 0000000..100749c --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sftp.js @@ -0,0 +1,488 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Config = imports.config; +const Lan = imports.service.backends.lan; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('SFTP'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SFTP', + description: _('Browse the paired device filesystem'), + incomingCapabilities: ['kdeconnect.sftp'], + outgoingCapabilities: ['kdeconnect.sftp.request'], + actions: { + mount: { + label: _('Mount'), + icon_name: 'folder-remote-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.sftp'], + outgoing: ['kdeconnect.sftp.request'], + }, + unmount: { + label: _('Unmount'), + icon_name: 'media-eject-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.sftp'], + outgoing: ['kdeconnect.sftp.request'], + }, + }, +}; + + +const MAX_MOUNT_DIRS = 12; + + +/** + * SFTP Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sftp + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SftpPlugin + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSFTPPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'sftp'); + + this._gmount = null; + this._mounting = false; + + // A reusable launcher for ssh processes + this._launcher = new Gio.SubprocessLauncher({ + flags: (Gio.SubprocessFlags.STDOUT_PIPE | + Gio.SubprocessFlags.STDERR_MERGE), + }); + + // Watch the volume monitor + this._volumeMonitor = Gio.VolumeMonitor.get(); + + this._mountAddedId = this._volumeMonitor.connect( + 'mount-added', + this._onMountAdded.bind(this) + ); + + this._mountRemovedId = this._volumeMonitor.connect( + 'mount-removed', + this._onMountRemoved.bind(this) + ); + } + + get gmount() { + if (this._gmount === null && this.device.connected) { + const host = this.device.channel.host; + + const regex = new RegExp( + `sftp://(${host}):(1739|17[4-5][0-9]|176[0-4])` + ); + + for (const mount of this._volumeMonitor.get_mounts()) { + const uri = mount.get_root().get_uri(); + + if (regex.test(uri)) { + this._gmount = mount; + this._addSubmenu(mount); + this._addSymlink(mount); + + break; + } + } + } + + return this._gmount; + } + + connected() { + super.connected(); + + // Only enable for Lan connections + if (this.device.channel instanceof Lan.Channel) { + if (this.settings.get_boolean('automount')) + this.mount(); + } else { + this.device.lookup_action('mount').enabled = false; + this.device.lookup_action('unmount').enabled = false; + } + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.sftp': + if (packet.body.hasOwnProperty('errorMessage')) + this._handleError(packet); + else + this._handleMount(packet); + + break; + } + } + + _onMountAdded(monitor, mount) { + if (this._gmount !== null || !this.device.connected) + return; + + const host = this.device.channel.host; + const regex = new RegExp(`sftp://(${host}):(1739|17[4-5][0-9]|176[0-4])`); + const uri = mount.get_root().get_uri(); + + if (!regex.test(uri)) + return; + + this._gmount = mount; + this._addSubmenu(mount); + this._addSymlink(mount); + } + + _onMountRemoved(monitor, mount) { + if (this.gmount !== mount) + return; + + this._gmount = null; + this._removeSubmenu(); + } + + async _listDirectories(mount) { + const file = mount.get_root(); + + const iter = await file.enumerate_children_async( + Gio.FILE_ATTRIBUTE_STANDARD_NAME, + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + this.cancellable); + + const infos = await iter.next_files_async(MAX_MOUNT_DIRS, + GLib.PRIORITY_DEFAULT, this.cancellable); + iter.close_async(GLib.PRIORITY_DEFAULT, null, null); + + const directories = {}; + + for (const info of infos) { + const name = info.get_name(); + directories[name] = `${file.get_uri()}${name}/`; + } + + return directories; + } + + _onAskQuestion(op, message, choices) { + op.reply(Gio.MountOperationResult.HANDLED); + } + + _onAskPassword(op, message, user, domain, flags) { + op.reply(Gio.MountOperationResult.HANDLED); + } + + /** + * Handle an error reported by the remote device. + * + * @param {Core.Packet} packet - a `kdeconnect.sftp` + */ + _handleError(packet) { + this.device.showNotification({ + id: 'sftp-error', + title: _('%s reported an error').format(this.device.name), + body: packet.body.errorMessage, + icon: new Gio.ThemedIcon({name: 'dialog-error-symbolic'}), + priority: Gio.NotificationPriority.HIGH, + }); + } + + /** + * Mount the remote device using the provided information. + * + * @param {Core.Packet} packet - a `kdeconnect.sftp` + */ + async _handleMount(packet) { + try { + // Already mounted or mounting + if (this.gmount !== null || this._mounting) + return; + + this._mounting = true; + + // Ensure the private key is in the keyring + await this._addPrivateKey(); + + // Create a new mount operation + const op = new Gio.MountOperation({ + username: packet.body.user || null, + password: packet.body.password || null, + password_save: Gio.PasswordSave.NEVER, + }); + + op.connect('ask-question', this._onAskQuestion); + op.connect('ask-password', this._onAskPassword); + + // This is the actual call to mount the device + const host = this.device.channel.host; + const uri = `sftp://${host}:${packet.body.port}/`; + const file = Gio.File.new_for_uri(uri); + + await file.mount_enclosing_volume(GLib.PRIORITY_DEFAULT, op, + this.cancellable); + } catch (e) { + // Special case when the GMount didn't unmount properly but is still + // on the same port and can be reused. + if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ALREADY_MOUNTED)) + return; + + // There's a good chance this is a host key verification error; + // regardless we'll remove the key for security. + this._removeHostKey(this.device.channel.host); + logError(e, this.device.name); + } finally { + this._mounting = false; + } + } + + /** + * Add GSConnect's private key identity to the authentication agent so our + * identity can be verified by Android during private key authentication. + * + * @return {Promise} A promise for the operation + */ + async _addPrivateKey() { + const ssh_add = this._launcher.spawnv([ + Config.SSHADD_PATH, + GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']), + ]); + + const [stdout] = await ssh_add.communicate_utf8_async(null, + this.cancellable); + + if (ssh_add.get_exit_status() !== 0) + debug(stdout.trim(), this.device.name); + } + + /** + * Remove all host keys from ~/.ssh/known_hosts for @host in the port range + * used by KDE Connect (1739-1764). + * + * @param {string} host - A hostname or IP address + */ + async _removeHostKey(host) { + for (let port = 1739; port <= 1764; port++) { + try { + const ssh_keygen = this._launcher.spawnv([ + Config.SSHKEYGEN_PATH, + '-R', + `[${host}]:${port}`, + ]); + + const [stdout] = await ssh_keygen.communicate_utf8_async(null, + this.cancellable); + + const status = ssh_keygen.get_exit_status(); + + if (status !== 0) { + throw new Gio.IOErrorEnum({ + code: Gio.io_error_from_errno(status), + message: `${GLib.strerror(status)}\n${stdout}`.trim(), + }); + } + } catch (e) { + logError(e, this.device.name); + } + } + } + + /* + * Mount menu helpers + */ + _getUnmountSection() { + if (this._unmountSection === undefined) { + this._unmountSection = new Gio.Menu(); + + const unmountItem = new Gio.MenuItem(); + unmountItem.set_label(Metadata.actions.unmount.label); + unmountItem.set_icon(new Gio.ThemedIcon({ + name: Metadata.actions.unmount.icon_name, + })); + unmountItem.set_detailed_action('device.unmount'); + this._unmountSection.append_item(unmountItem); + } + + return this._unmountSection; + } + + _getFilesMenuItem() { + if (this._filesMenuItem === undefined) { + // Files menu icon + const emblem = new Gio.Emblem({ + icon: new Gio.ThemedIcon({name: 'emblem-default'}), + }); + + const mountedIcon = new Gio.EmblemedIcon({ + gicon: new Gio.ThemedIcon({name: 'folder-remote-symbolic'}), + }); + mountedIcon.add_emblem(emblem); + + // Files menu item + this._filesMenuItem = new Gio.MenuItem(); + this._filesMenuItem.set_detailed_action('device.mount'); + this._filesMenuItem.set_icon(mountedIcon); + this._filesMenuItem.set_label(_('Files')); + } + + return this._filesMenuItem; + } + + async _addSubmenu(mount) { + try { + const directories = await this._listDirectories(mount); + + // Submenu sections + const dirSection = new Gio.Menu(); + const unmountSection = this._getUnmountSection(); + + for (const [name, uri] of Object.entries(directories)) + dirSection.append(name, `device.openPath::${uri}`); + + // Files submenu + const filesSubmenu = new Gio.Menu(); + filesSubmenu.append_section(null, dirSection); + filesSubmenu.append_section(null, unmountSection); + + // Files menu item + const filesMenuItem = this._getFilesMenuItem(); + filesMenuItem.set_submenu(filesSubmenu); + + // Replace the existing menu item + const index = this.device.removeMenuAction('device.mount'); + this.device.addMenuItem(filesMenuItem, index); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) + debug(e, this.device.name); + + // Reset to allow retrying + this._gmount = null; + } + } + + _removeSubmenu() { + try { + const index = this.device.removeMenuAction('device.mount'); + const action = this.device.lookup_action('mount'); + + if (action !== null) { + this.device.addMenuAction( + action, + index, + Metadata.actions.mount.label, + Metadata.actions.mount.icon_name + ); + } + } catch (e) { + logError(e, this.device.name); + } + } + + /** + * Create a symbolic link referring to the device by name + * + * @param {Gio.Mount} mount - A GMount to link to + */ + async _addSymlink(mount) { + try { + const by_name_dir = Gio.File.new_for_path( + `${Config.RUNTIMEDIR}/by-name/` + ); + + try { + by_name_dir.make_directory_with_parents(null); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) + throw e; + } + + // Replace path separator with a Unicode lookalike: + let safe_device_name = this.device.name.replace('/', '∕'); + + if (safe_device_name === '.') + safe_device_name = '·'; + else if (safe_device_name === '..') + safe_device_name = '··'; + + const link_target = mount.get_root().get_path(); + const link = Gio.File.new_for_path( + `${by_name_dir.get_path()}/${safe_device_name}`); + + // Check for and remove any existing stale link + try { + const link_stat = await link.query_info_async( + 'standard::symlink-target', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + GLib.PRIORITY_DEFAULT, + this.cancellable); + + if (link_stat.get_symlink_target() === link_target) + return; + + await link.delete_async(GLib.PRIORITY_DEFAULT, + this.cancellable); + } catch (e) { + if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) + throw e; + } + + link.make_symbolic_link(link_target, this.cancellable); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Send a request to mount the remote device + */ + mount() { + if (this.gmount !== null) + return; + + this.device.sendPacket({ + type: 'kdeconnect.sftp.request', + body: { + startBrowsing: true, + }, + }); + } + + /** + * Remove the menu items, unmount the filesystem, replace the mount item + */ + async unmount() { + try { + if (this.gmount === null) + return; + + this._removeSubmenu(); + this._mounting = false; + + await this.gmount.unmount_with_operation( + Gio.MountUnmountFlags.FORCE, + new Gio.MountOperation(), + this.cancellable); + } catch (e) { + debug(e, this.device.name); + } + } + + destroy() { + if (this._volumeMonitor) { + this._volumeMonitor.disconnect(this._mountAddedId); + this._volumeMonitor.disconnect(this._mountRemovedId); + this._volumeMonitor = null; + } + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js new file mode 100644 index 0000000..af0eae6 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/share.js @@ -0,0 +1,492 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +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 PluginBase = imports.service.plugin; +const URI = imports.service.utils.uri; + + +var Metadata = { + label: _('Share'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Share', + description: _('Share files and URLs between devices'), + incomingCapabilities: ['kdeconnect.share.request'], + outgoingCapabilities: ['kdeconnect.share.request'], + actions: { + share: { + label: _('Share'), + icon_name: 'send-to-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareFile: { + label: _('Share File'), + icon_name: 'document-send-symbolic', + + parameter_type: new GLib.VariantType('(sb)'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareText: { + label: _('Share Text'), + icon_name: 'send-to-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + shareUri: { + label: _('Share Link'), + icon_name: 'send-to-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.share.request'], + }, + }, +}; + + +/** + * Share Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/share + * + * TODO: receiving 'text' TODO: Window with textview & 'Copy to Clipboard.. + * https://github.com/KDE/kdeconnect-kde/commit/28f11bd5c9a717fb9fbb3f02ddd6cea62021d055 + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSharePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'share'); + } + + handlePacket(packet) { + // TODO: composite jobs (lastModified, numberOfFiles, totalPayloadSize) + if (packet.body.hasOwnProperty('filename')) { + if (this.settings.get_boolean('receive-files')) + this._handleFile(packet); + else + this._refuseFile(packet); + } else if (packet.body.hasOwnProperty('text')) { + this._handleText(packet); + } else if (packet.body.hasOwnProperty('url')) { + this._handleUri(packet); + } + } + + _ensureReceiveDirectory() { + let receiveDir = this.settings.get_string('receive-directory'); + + // Ensure a directory is set + if (receiveDir.length === 0) { + receiveDir = GLib.get_user_special_dir( + GLib.UserDirectory.DIRECTORY_DOWNLOAD + ); + + // Fallback to ~/Downloads + const homeDir = GLib.get_home_dir(); + + if (!receiveDir || receiveDir === homeDir) + receiveDir = GLib.build_filenamev([homeDir, 'Downloads']); + + this.settings.set_string('receive-directory', receiveDir); + } + + // Ensure the directory exists + if (!GLib.file_test(receiveDir, GLib.FileTest.IS_DIR)) + GLib.mkdir_with_parents(receiveDir, 448); + + return receiveDir; + } + + _getFile(filename) { + const dirpath = this._ensureReceiveDirectory(); + const basepath = GLib.build_filenamev([dirpath, filename]); + let filepath = basepath; + let copyNum = 0; + + while (GLib.file_test(filepath, GLib.FileTest.EXISTS)) + filepath = `${basepath} (${++copyNum})`; + + return Gio.File.new_for_path(filepath); + } + + _refuseFile(packet) { + try { + this.device.rejectTransfer(packet); + + this.device.showNotification({ + id: `${Date.now()}`, + title: _('Transfer Failed'), + // TRANSLATORS: eg. Google Pixel is not allowed to upload files + body: _('%s is not allowed to upload files').format( + this.device.name + ), + icon: new Gio.ThemedIcon({name: 'dialog-error-symbolic'}), + }); + } catch (e) { + debug(e, this.device.name); + } + } + + async _handleFile(packet) { + try { + const file = this._getFile(packet.body.filename); + + // Create the transfer + const transfer = this.device.createTransfer(); + + transfer.addFile(packet, file); + + // Notify that we're about to start the transfer + this.device.showNotification({ + id: transfer.uuid, + title: _('Transferring File'), + // TRANSLATORS: eg. Receiving 'book.pdf' from Google Pixel + body: _('Receiving “%s” from %s').format( + packet.body.filename, + this.device.name + ), + buttons: [{ + label: _('Cancel'), + action: 'cancelTransfer', + parameter: new GLib.Variant('s', transfer.uuid), + }], + icon: new Gio.ThemedIcon({name: 'document-save-symbolic'}), + }); + + // We'll show a notification (success or failure) + let title, body, action, iconName; + let buttons = []; + + try { + await transfer.start(); + + title = _('Transfer Successful'); + // TRANSLATORS: eg. Received 'book.pdf' from Google Pixel + body = _('Received “%s” from %s').format( + packet.body.filename, + this.device.name + ); + action = { + name: 'showPathInFolder', + parameter: new GLib.Variant('s', file.get_uri()), + }; + buttons = [ + { + label: _('Show File Location'), + action: 'showPathInFolder', + parameter: new GLib.Variant('s', file.get_uri()), + }, + { + label: _('Open File'), + action: 'openPath', + parameter: new GLib.Variant('s', file.get_uri()), + }, + ]; + iconName = 'document-save-symbolic'; + + if (packet.body.open) { + const uri = file.get_uri(); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + } catch (e) { + debug(e, this.device.name); + + title = _('Transfer Failed'); + // TRANSLATORS: eg. Failed to receive 'book.pdf' from Google Pixel + body = _('Failed to receive “%s” from %s').format( + packet.body.filename, + this.device.name + ); + iconName = 'dialog-warning-symbolic'; + + // Clean up the downloaded file on failure + file.delete_async(GLib.PRIORITY_DEAFAULT, null, null); + } + + this.device.hideNotification(transfer.uuid); + this.device.showNotification({ + id: transfer.uuid, + title: title, + body: body, + action: action, + buttons: buttons, + icon: new Gio.ThemedIcon({name: iconName}), + }); + } catch (e) { + logError(e, this.device.name); + } + } + + _handleUri(packet) { + const uri = packet.body.url; + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + _handleText(packet) { + const dialog = new Gtk.MessageDialog({ + text: _('Text Shared By %s').format(this.device.name), + secondary_text: URI.linkify(packet.body.text), + secondary_use_markup: true, + buttons: Gtk.ButtonsType.CLOSE, + }); + dialog.message_area.get_children()[1].selectable = true; + dialog.set_keep_above(true); + dialog.connect('response', (dialog) => dialog.destroy()); + dialog.show(); + } + + /** + * Open the file chooser dialog for selecting a file or inputing a URI. + */ + share() { + const dialog = new FileChooserDialog(this.device); + dialog.show(); + } + + /** + * Share local file path or URI + * + * @param {string} path - Local file path or URI + * @param {boolean} open - Whether the file should be opened after transfer + */ + async shareFile(path, open = false) { + try { + let file = null; + + if (path.includes('://')) + file = Gio.File.new_for_uri(path); + else + file = Gio.File.new_for_path(path); + + // Create the transfer + const transfer = this.device.createTransfer(); + + transfer.addFile({ + type: 'kdeconnect.share.request', + body: { + filename: file.get_basename(), + open: open, + }, + }, file); + + // Notify that we're about to start the transfer + this.device.showNotification({ + id: transfer.uuid, + title: _('Transferring File'), + // TRANSLATORS: eg. Sending 'book.pdf' to Google Pixel + body: _('Sending “%s” to %s').format( + file.get_basename(), + this.device.name + ), + buttons: [{ + label: _('Cancel'), + action: 'cancelTransfer', + parameter: new GLib.Variant('s', transfer.uuid), + }], + icon: new Gio.ThemedIcon({name: 'document-send-symbolic'}), + }); + + // We'll show a notification (success or failure) + let title, body, iconName; + + try { + await transfer.start(); + + title = _('Transfer Successful'); + // TRANSLATORS: eg. Sent "book.pdf" to Google Pixel + body = _('Sent “%s” to %s').format( + file.get_basename(), + this.device.name + ); + iconName = 'document-send-symbolic'; + } catch (e) { + debug(e, this.device.name); + + title = _('Transfer Failed'); + // TRANSLATORS: eg. Failed to send "book.pdf" to Google Pixel + body = _('Failed to send “%s” to %s').format( + file.get_basename(), + this.device.name + ); + iconName = 'dialog-warning-symbolic'; + } + + this.device.hideNotification(transfer.uuid); + this.device.showNotification({ + id: transfer.uuid, + title: title, + body: body, + icon: new Gio.ThemedIcon({name: iconName}), + }); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Share a string of text. Remote behaviour is undefined. + * + * @param {string} text - A string of unicode text + */ + shareText(text) { + this.device.sendPacket({ + type: 'kdeconnect.share.request', + body: {text: text}, + }); + } + + /** + * Share a URI. Generally the remote device opens it with the scheme default + * + * @param {string} uri - A URI to share + */ + shareUri(uri) { + if (GLib.uri_parse_scheme(uri) === 'file') { + this.shareFile(uri); + return; + } + + this.device.sendPacket({ + type: 'kdeconnect.share.request', + body: {url: uri}, + }); + } +}); + + +/** A simple FileChooserDialog for sharing files */ +var FileChooserDialog = GObject.registerClass({ + GTypeName: 'GSConnectShareFileChooserDialog', +}, class FileChooserDialog extends Gtk.FileChooserDialog { + + _init(device) { + super._init({ + // TRANSLATORS: eg. Send files to Google Pixel + title: _('Send files to %s').format(device.name), + select_multiple: true, + extra_widget: new Gtk.CheckButton({ + // TRANSLATORS: Mark the file to be opened once completed + label: _('Open when done'), + visible: true, + }), + use_preview_label: false, + }); + + this.device = device; + + // Align checkbox with sidebar + const box = this.get_content_area().get_children()[0].get_children()[0]; + const paned = box.get_children()[0]; + paned.bind_property( + 'position', + this.extra_widget, + 'margin-left', + GObject.BindingFlags.SYNC_CREATE + ); + + // Preview Widget + this.preview_widget = new Gtk.Image(); + this.preview_widget_active = false; + this.connect('update-preview', this._onUpdatePreview); + + // URI entry + this._uriEntry = new Gtk.Entry({ + placeholder_text: 'https://', + hexpand: true, + visible: true, + }); + this._uriEntry.connect('activate', this._sendLink.bind(this)); + + // URI/File toggle + this._uriButton = new Gtk.ToggleButton({ + image: new Gtk.Image({ + icon_name: 'web-browser-symbolic', + pixel_size: 16, + }), + valign: Gtk.Align.CENTER, + // TRANSLATORS: eg. Send a link to Google Pixel + tooltip_text: _('Send a link to %s').format(device.name), + visible: true, + }); + this._uriButton.connect('toggled', this._onUriButtonToggled.bind(this)); + + this.add_button(_('Cancel'), Gtk.ResponseType.CANCEL); + const sendButton = this.add_button(_('Send'), Gtk.ResponseType.OK); + sendButton.connect('clicked', this._sendLink.bind(this)); + + this.get_header_bar().pack_end(this._uriButton); + this.set_default_response(Gtk.ResponseType.OK); + } + + _onUpdatePreview(chooser) { + try { + const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size( + chooser.get_preview_filename(), + chooser.get_scale_factor() * 128, + -1 + ); + chooser.preview_widget.pixbuf = pixbuf; + chooser.preview_widget.visible = true; + chooser.preview_widget_active = true; + } catch (e) { + chooser.preview_widget.visible = false; + chooser.preview_widget_active = false; + } + } + + _onUriButtonToggled(button) { + const header = this.get_header_bar(); + + // Show the URL entry + if (button.active) { + this.extra_widget.sensitive = false; + header.set_custom_title(this._uriEntry); + this.set_response_sensitive(Gtk.ResponseType.OK, true); + + // Hide the URL entry + } else { + header.set_custom_title(null); + this.set_response_sensitive( + Gtk.ResponseType.OK, + this.get_uris().length > 1 + ); + this.extra_widget.sensitive = true; + } + } + + _sendLink(widget) { + if (this._uriButton.active && this._uriEntry.text.length) + this.response(1); + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + for (const uri of this.get_uris()) { + const parameter = new GLib.Variant( + '(sb)', + [uri, this.extra_widget.active] + ); + this.device.activate_action('shareFile', parameter); + } + } else if (response_id === 1) { + const parameter = new GLib.Variant('s', this._uriEntry.text); + this.device.activate_action('shareUri', parameter); + } + + this.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js new file mode 100644 index 0000000..5473209 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/sms.js @@ -0,0 +1,536 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const PluginBase = imports.service.plugin; +const LegacyMessaging = imports.service.ui.legacyMessaging; +const Messaging = imports.service.ui.messaging; +const URI = imports.service.utils.uri; + + +var Metadata = { + label: _('SMS'), + description: _('Send and read SMS of the paired device and be notified of new SMS'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SMS', + incomingCapabilities: [ + 'kdeconnect.sms.messages', + ], + outgoingCapabilities: [ + 'kdeconnect.sms.request', + 'kdeconnect.sms.request_conversation', + 'kdeconnect.sms.request_conversations', + ], + actions: { + // SMS Actions + sms: { + label: _('Messaging'), + icon_name: 'sms-symbolic', + + parameter_type: null, + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + uriSms: { + label: _('New SMS (URI)'), + icon_name: 'sms-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + replySms: { + label: _('Reply SMS'), + icon_name: 'sms-symbolic', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + sendMessage: { + label: _('Send Message'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('(aa{sv})'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + sendSms: { + label: _('Send SMS'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('(ss)'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + shareSms: { + label: _('Share SMS'), + icon_name: 'sms-send', + + parameter_type: new GLib.VariantType('s'), + incoming: [], + outgoing: ['kdeconnect.sms.request'], + }, + }, +}; + + +/** + * SMS Message event type. Currently all events are TEXT_MESSAGE. + * + * TEXT_MESSAGE: Has a "body" field which contains pure, human-readable text + */ +var MessageEventType = { + TEXT_MESSAGE: 0x1, +}; + + +/** + * SMS Message status. READ/UNREAD match the 'read' field from the Android App + * message packet. + * + * UNREAD: A message not marked as read + * READ: A message marked as read + */ +var MessageStatus = { + UNREAD: 0, + READ: 1, +}; + + +/** + * SMS Message type, set from the 'type' field in the Android App + * message packet. + * + * See: https://developer.android.com/reference/android/provider/Telephony.TextBasedSmsColumns.html + * + * ALL: all messages + * INBOX: Received messages + * SENT: Sent messages + * DRAFT: Message drafts + * OUTBOX: Outgoing messages + * FAILED: Failed outgoing messages + * QUEUED: Messages queued to send later + */ +var MessageBox = { + ALL: 0, + INBOX: 1, + SENT: 2, + DRAFT: 3, + OUTBOX: 4, + FAILED: 5, + QUEUED: 6, +}; + + +/** + * SMS Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/sms + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SMSPlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSMSPlugin', + Properties: { + 'threads': GObject.param_spec_variant( + 'threads', + 'Conversation List', + 'A list of threads', + new GLib.VariantType('aa{sv}'), + null, + GObject.ParamFlags.READABLE + ), + }, +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'sms'); + + this.cacheProperties(['_threads']); + } + + get threads() { + if (this._threads === undefined) + this._threads = {}; + + return this._threads; + } + + get window() { + if (this.settings.get_boolean('legacy-sms')) { + return new LegacyMessaging.Dialog({ + device: this.device, + plugin: this, + }); + } + + if (this._window === undefined) { + this._window = new Messaging.Window({ + application: Gio.Application.get_default(), + device: this.device, + plugin: this, + }); + + this._window.connect('destroy', () => { + this._window = undefined; + }); + } + + return this._window; + } + + clearCache() { + this._threads = {}; + this.notify('threads'); + } + + cacheLoaded() { + this.notify('threads'); + } + + connected() { + super.connected(); + this._requestConversations(); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.sms.messages': + this._handleMessages(packet.body.messages); + break; + } + } + + /** + * Handle a digest of threads. + * + * @param {Object[]} messages - A list of message objects + * @param {string[]} thread_ids - A list of thread IDs as strings + */ + _handleDigest(messages, thread_ids) { + // Prune threads + for (const thread_id of Object.keys(this.threads)) { + if (!thread_ids.includes(thread_id)) + delete this.threads[thread_id]; + } + + // Request each new or newer thread + for (let i = 0, len = messages.length; i < len; i++) { + const message = messages[i]; + const cache = this.threads[message.thread_id]; + + if (cache === undefined) { + this._requestConversation(message.thread_id); + continue; + } + + // If this message is marked read, mark the rest as read + if (message.read === MessageStatus.READ) { + for (const msg of cache) + msg.read = MessageStatus.READ; + } + + // If we don't have a thread for this message or it's newer + // than the last message in the cache, request the thread + if (!cache.length || cache[cache.length - 1].date < message.date) + this._requestConversation(message.thread_id); + } + + this.notify('threads'); + } + + /** + * Handle a new single message + * + * @param {Object} message - A message object + */ + _handleMessage(message) { + let conversation = null; + + // If the window is open, try and find an active conversation + if (this._window) + conversation = this._window.getConversationForMessage(message); + + // If there's an active conversation, we should log the message now + if (conversation) + conversation.logNext(message); + } + + /** + * Parse a conversation (thread of messages) and sort them + * + * @param {Object[]} thread - A list of sms message objects from a thread + */ + _handleThread(thread) { + // If there are no addresses this will cause major problems... + if (!thread[0].addresses || !thread[0].addresses[0]) + return; + + const thread_id = thread[0].thread_id; + const cache = this.threads[thread_id] || []; + + // Handle each message + for (let i = 0, len = thread.length; i < len; i++) { + const message = thread[i]; + + // TODO: We only cache messages of a known MessageBox since we + // have no reliable way to determine its direction, let alone + // what to do with it. + if (message.type < 0 || message.type > 6) + continue; + + // If the message exists, just update it + const cacheMessage = cache.find(m => m.date === message.date); + + if (cacheMessage) { + Object.assign(cacheMessage, message); + } else { + cache.push(message); + this._handleMessage(message); + } + } + + // Sort the thread by ascending date and notify + this.threads[thread_id] = cache.sort((a, b) => a.date - b.date); + this.notify('threads'); + } + + /** + * Handle a response to telephony.request_conversation(s) + * + * @param {Object[]} messages - A list of sms message objects + */ + _handleMessages(messages) { + try { + // If messages is empty there's nothing to do... + if (messages.length === 0) + return; + + const thread_ids = []; + + // Perform some modification of the messages + for (let i = 0, len = messages.length; i < len; i++) { + const message = messages[i]; + + // COERCION: thread_id's to strings + message.thread_id = `${message.thread_id}`; + thread_ids.push(message.thread_id); + + // TODO: Remove bogus `insert-address-token` entries + let a = message.addresses.length; + + while (a--) { + if (message.addresses[a].address === undefined || + message.addresses[a].address === 'insert-address-token') + message.addresses.splice(a, 1); + } + } + + // If there's multiple thread_id's it's a summary of threads + if (thread_ids.some(id => id !== thread_ids[0])) + this._handleDigest(messages, thread_ids); + + // Otherwise this is single thread or new message + else + this._handleThread(messages); + } catch (e) { + debug(e, this.device.name); + } + } + + /** + * Request a list of messages from a single thread. + * + * @param {number} thread_id - The id of the thread to request + */ + _requestConversation(thread_id) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request_conversation', + body: { + threadID: thread_id, + }, + }); + } + + /** + * Request a list of the last message in each unarchived thread. + */ + _requestConversations() { + this.device.sendPacket({ + type: 'kdeconnect.sms.request_conversations', + }); + } + + /** + * A notification action for replying to SMS messages (or missed calls). + * + * @param {string} hint - Could be either a contact name or phone number + */ + replySms(hint) { + this.window.present(); + // FIXME: causes problems now that non-numeric addresses are allowed + // this.window.address = hint.toPhoneNumber(); + } + + /** + * Send an SMS message + * + * @param {string} phoneNumber - The phone number to send the message to + * @param {string} messageBody - The message to send + */ + sendSms(phoneNumber, messageBody) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request', + body: { + sendSms: true, + phoneNumber: phoneNumber, + messageBody: messageBody, + }, + }); + } + + /** + * Send a message + * + * @param {Object[]} addresses - A list of address objects + * @param {string} messageBody - The message text + * @param {number} [event] - An event bitmask + * @param {boolean} [forceSms] - Whether to force SMS + * @param {number} [subId] - The SIM card to use + */ + sendMessage(addresses, messageBody, event = 1, forceSms = false, subId = undefined) { + // TODO: waiting on support in kdeconnect-android + // if (this._version === 1) { + this.device.sendPacket({ + type: 'kdeconnect.sms.request', + body: { + sendSms: true, + phoneNumber: addresses[0].address, + messageBody: messageBody, + }, + }); + // } else if (this._version === 2) { + // this.device.sendPacket({ + // type: 'kdeconnect.sms.request', + // body: { + // version: 2, + // addresses: addresses, + // messageBody: messageBody, + // forceSms: forceSms, + // sub_id: subId + // } + // }); + // } + } + + /** + * Share a text content by SMS message. This is used by the WebExtension to + * share URLs from the browser, but could be used to initiate sharing of any + * text content. + * + * @param {string} url - The link to be shared + */ + shareSms(url) { + // Legacy Mode + if (this.settings.get_boolean('legacy-sms')) { + const window = this.window; + window.present(); + window.setMessage(url); + + // If there are active threads, show the chooser dialog + } else if (Object.values(this.threads).length > 0) { + const window = new Messaging.ConversationChooser({ + application: Gio.Application.get_default(), + device: this.device, + message: url, + plugin: this, + }); + + window.present(); + + // Otherwise show the window and wait for a contact to be chosen + } else { + this.window.present(); + this.window.setMessage(url, true); + } + } + + /** + * Open and present the messaging window + */ + sms() { + this.window.present(); + } + + /** + * This is the sms: URI scheme handler + * + * @param {string} uri - The URI the handle (sms:|sms://|sms:///) + */ + uriSms(uri) { + try { + uri = new URI.SmsURI(uri); + + // Lookup contacts + const addresses = uri.recipients.map(number => { + return {address: number.toPhoneNumber()}; + }); + const contacts = this.device.contacts.lookupAddresses(addresses); + + // Present the window and show the conversation + const window = this.window; + window.present(); + window.setContacts(contacts); + + // Set the outgoing message if the uri has a body variable + if (uri.body) + window.setMessage(uri.body); + } catch (e) { + debug(e, `${this.device.name}: "${uri}"`); + } + } + + _threadHasAddress(thread, addressObj) { + const number = addressObj.address.toPhoneNumber(); + + for (const taddressObj of thread[0].addresses) { + const tnumber = taddressObj.address.toPhoneNumber(); + + if (number.endsWith(tnumber) || tnumber.endsWith(number)) + return true; + } + + return false; + } + + /** + * Try to find a thread_id in @smsPlugin for @addresses. + * + * @param {Object[]} addresses - a list of address objects + * @return {string|null} a thread ID + */ + getThreadIdForAddresses(addresses = []) { + const threads = Object.values(this.threads); + + for (const thread of threads) { + if (addresses.length !== thread[0].addresses.length) + continue; + + if (addresses.every(addressObj => this._threadHasAddress(thread, addressObj))) + return thread[0].thread_id; + } + + return null; + } + + destroy() { + if (this._window !== undefined) + this._window.destroy(); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js new file mode 100644 index 0000000..bf1783c --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/systemvolume.js @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const Config = imports.config; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('System Volume'), + description: _('Enable the paired device to control the system volume'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.SystemVolume', + incomingCapabilities: ['kdeconnect.systemvolume.request'], + outgoingCapabilities: ['kdeconnect.systemvolume'], + actions: {}, +}; + + +/** + * SystemVolume Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/systemvolume + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/SystemvolumePlugin/ + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectSystemVolumePlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'systemvolume'); + + // Cache stream properties + this._cache = new WeakMap(); + + // Connect to the mixer + try { + this._mixer = Components.acquire('pulseaudio'); + + this._streamChangedId = this._mixer.connect( + 'stream-changed', + this._sendSink.bind(this) + ); + + this._outputAddedId = this._mixer.connect( + 'output-added', + this._sendSinkList.bind(this) + ); + + this._outputRemovedId = this._mixer.connect( + 'output-removed', + this._sendSinkList.bind(this) + ); + + // Modify the error to redirect to the wiki + } catch (e) { + e.name = _('PulseAudio not found'); + e.url = `${Config.PACKAGE_URL}/wiki/Error#pulseaudio-not-found`; + throw e; + } + } + + handlePacket(packet) { + switch (true) { + case packet.body.hasOwnProperty('requestSinks'): + this._sendSinkList(); + break; + + case packet.body.hasOwnProperty('name'): + this._changeSink(packet); + break; + } + } + + connected() { + super.connected(); + + this._sendSinkList(); + } + + /** + * Handle a request to change an output + * + * @param {Core.Packet} packet - a `kdeconnect.systemvolume.request` + */ + _changeSink(packet) { + let stream; + + for (const sink of this._mixer.get_sinks()) { + if (sink.name === packet.body.name) { + stream = sink; + break; + } + } + + // No sink with the given name + if (stream === undefined) { + this._sendSinkList(); + return; + } + + // Get a cache and store volume and mute states if changed + const cache = this._cache.get(stream) || {}; + + if (packet.body.hasOwnProperty('muted')) { + cache.muted = packet.body.muted; + this._cache.set(stream, cache); + stream.change_is_muted(packet.body.muted); + } + + if (packet.body.hasOwnProperty('volume')) { + cache.volume = packet.body.volume; + this._cache.set(stream, cache); + stream.volume = packet.body.volume; + stream.push_volume(); + } + } + + /** + * Update the cache for @stream + * + * @param {Gvc.MixerStream} stream - The stream to cache + * @return {Object} The updated cache object + */ + _updateCache(stream) { + const state = { + name: stream.name, + description: stream.display_name, + muted: stream.is_muted, + volume: stream.volume, + maxVolume: this._mixer.get_vol_max_norm(), + }; + + this._cache.set(stream, state); + + return state; + } + + /** + * Send the state of a local sink + * + * @param {Gvc.MixerControl} mixer - The mixer that owns the stream + * @param {number} id - The Id of the stream that changed + */ + _sendSink(mixer, id) { + // Avoid starving the packet channel when fading + if (this._mixer.fading) + return; + + // Check the cache + const stream = this._mixer.lookup_stream_id(id); + const cache = this._cache.get(stream) || {}; + + // If the port has changed we have to send the whole list to update the + // display name + if (!cache.display_name || cache.display_name !== stream.display_name) { + this._sendSinkList(); + return; + } + + // If only volume and/or mute are set, send a single update + if (cache.volume !== stream.volume || cache.muted !== stream.is_muted) { + // Update the cache + const state = this._updateCache(stream); + + // Send the stream update + this.device.sendPacket({ + type: 'kdeconnect.systemvolume', + body: state, + }); + } + } + + /** + * Send a list of local sinks + */ + _sendSinkList() { + const sinkList = this._mixer.get_sinks().map(sink => { + return this._updateCache(sink); + }); + + // Send the sinkList + this.device.sendPacket({ + type: 'kdeconnect.systemvolume', + body: { + sinkList: sinkList, + }, + }); + } + + destroy() { + if (this._mixer !== undefined) { + this._mixer.disconnect(this._streamChangedId); + this._mixer.disconnect(this._outputAddedId); + this._mixer.disconnect(this._outputRemovedId); + this._mixer = Components.release('pulseaudio'); + } + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js new file mode 100644 index 0000000..9fe683a --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/plugins/telephony.js @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GdkPixbuf = imports.gi.GdkPixbuf; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Components = imports.service.components; +const PluginBase = imports.service.plugin; + + +var Metadata = { + label: _('Telephony'), + description: _('Be notified about calls and adjust system volume during ringing/ongoing calls'), + id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Telephony', + incomingCapabilities: [ + 'kdeconnect.telephony', + ], + outgoingCapabilities: [ + 'kdeconnect.telephony.request', + 'kdeconnect.telephony.request_mute', + ], + actions: { + muteCall: { + // TRANSLATORS: Silence the actively ringing call + label: _('Mute Call'), + icon_name: 'audio-volume-muted-symbolic', + + parameter_type: null, + incoming: ['kdeconnect.telephony'], + outgoing: ['kdeconnect.telephony.request_mute'], + }, + }, +}; + + +/** + * Telephony Plugin + * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/telephony + * https://github.com/KDE/kdeconnect-android/tree/master/src/org/kde/kdeconnect/Plugins/TelephonyPlugin + */ +var Plugin = GObject.registerClass({ + GTypeName: 'GSConnectTelephonyPlugin', +}, class Plugin extends PluginBase.Plugin { + + _init(device) { + super._init(device, 'telephony'); + + // Neither of these are crucial for the plugin to work + this._mpris = Components.acquire('mpris'); + this._mixer = Components.acquire('pulseaudio'); + } + + handlePacket(packet) { + switch (packet.type) { + case 'kdeconnect.telephony': + this._handleEvent(packet); + break; + } + } + + /** + * Change volume, microphone and media player state in response to an + * incoming or answered call. + * + * @param {string} eventType - 'ringing' or 'talking' + */ + _setMediaState(eventType) { + // Mixer Volume + if (this._mixer !== undefined) { + switch (this.settings.get_string(`${eventType}-volume`)) { + case 'restore': + this._mixer.restore(); + break; + + case 'lower': + this._mixer.lowerVolume(); + break; + + case 'mute': + this._mixer.muteVolume(); + break; + } + + if (eventType === 'talking' && this.settings.get_boolean('talking-microphone')) + this._mixer.muteMicrophone(); + } + + // Media Playback + if (this._mpris && this.settings.get_boolean(`${eventType}-pause`)) + this._mpris.pauseAll(); + } + + /** + * Restore volume, microphone and media player state (if changed), making + * sure to unpause before raising volume. + * + * TODO: there's a possibility we might revert a media/mixer state set for + * another device. + */ + _restoreMediaState() { + // Media Playback + if (this._mpris) + this._mpris.unpauseAll(); + + // Mixer Volume + if (this._mixer) + this._mixer.restore(); + } + + /** + * Load a Gdk.Pixbuf from base64 encoded data + * + * @param {string} data - Base64 encoded JPEG data + * @return {Gdk.Pixbuf|null} A contact photo + */ + _getThumbnailPixbuf(data) { + const loader = new GdkPixbuf.PixbufLoader(); + + try { + data = GLib.base64_decode(data); + loader.write(data); + loader.close(); + } catch (e) { + debug(e, this.device.name); + } + + return loader.get_pixbuf(); + } + + /** + * Handle a telephony event (ringing, talking), showing or hiding a + * notification and possibly adjusting the media/mixer state. + * + * @param {Core.Packet} packet - A `kdeconnect.telephony` + */ + _handleEvent(packet) { + // Only handle 'ringing' or 'talking' events; leave the notification + // plugin to handle 'missedCall' since they're often repliable + if (!['ringing', 'talking'].includes(packet.body.event)) + return; + + // This is the end of a telephony event + if (packet.body.isCancel) + this._cancelEvent(packet); + else + this._notifyEvent(packet); + } + + _cancelEvent(packet) { + // Ensure we have a sender + // TRANSLATORS: No name or phone number + let sender = _('Unknown Contact'); + + if (packet.body.contactName) + sender = packet.body.contactName; + else if (packet.body.phoneNumber) + sender = packet.body.phoneNumber; + + this.device.hideNotification(`${packet.body.event}|${sender}`); + this._restoreMediaState(); + } + + _notifyEvent(packet) { + let body; + let buttons = []; + let icon = null; + let priority = Gio.NotificationPriority.NORMAL; + + // Ensure we have a sender + // TRANSLATORS: No name or phone number + let sender = _('Unknown Contact'); + + if (packet.body.contactName) + sender = packet.body.contactName; + else if (packet.body.phoneNumber) + sender = packet.body.phoneNumber; + + // If there's a photo, use it as the notification icon + if (packet.body.phoneThumbnail) + icon = this._getThumbnailPixbuf(packet.body.phoneThumbnail); + + if (icon === null) + icon = new Gio.ThemedIcon({name: 'call-start-symbolic'}); + + // Notify based based on the event type + if (packet.body.event === 'ringing') { + this._setMediaState('ringing'); + + // TRANSLATORS: The phone is ringing + body = _('Incoming call'); + buttons = [{ + action: 'muteCall', + // TRANSLATORS: Silence the actively ringing call + label: _('Mute'), + parameter: null, + }]; + priority = Gio.NotificationPriority.URGENT; + } + + if (packet.body.event === 'talking') { + this.device.hideNotification(`ringing|${sender}`); + this._setMediaState('talking'); + + // TRANSLATORS: A phone call is active + body = _('Ongoing call'); + } + + this.device.showNotification({ + id: `${packet.body.event}|${sender}`, + title: sender, + body: body, + icon: icon, + priority: priority, + buttons: buttons, + }); + } + + /** + * Silence an incoming call and restore the previous mixer/media state, if + * applicable. + */ + muteCall() { + this.device.sendPacket({ + type: 'kdeconnect.telephony.request_mute', + body: {}, + }); + + this._restoreMediaState(); + } + + destroy() { + if (this._mixer !== undefined) + this._mixer = Components.release('pulseaudio'); + + if (this._mpris !== undefined) + this._mpris = Components.release('mpris'); + + super.destroy(); + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js new file mode 100644 index 0000000..090eac5 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/contacts.js @@ -0,0 +1,642 @@ +// 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 GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +/** + * Return a random color + * + * @param {*} [salt] - If not %null, will be used as salt for generating a color + * @param {number} alpha - A value in the [0...1] range for the alpha channel + * @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input + */ +function randomRGBA(salt = null, alpha = 1.0) { + let red, green, blue; + + if (salt !== null) { + const hash = new GLib.Variant('s', `${salt}`).hash(); + red = ((hash & 0xFF0000) >> 16) / 255; + green = ((hash & 0x00FF00) >> 8) / 255; + blue = (hash & 0x0000FF) / 255; + } else { + red = Math.random(); + green = Math.random(); + blue = Math.random(); + } + + return new Gdk.RGBA({red: red, green: green, blue: blue, alpha: alpha}); +} + + +/** + * Get the relative luminance of a RGB set + * See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + * + * @param {Gdk.RGBA} rgba - A GdkRGBA object + * @return {number} The relative luminance of the color + */ +function relativeLuminance(rgba) { + const {red, green, blue} = rgba; + + const R = (red > 0.03928) ? red / 12.92 : Math.pow(((red + 0.055) / 1.055), 2.4); + const G = (green > 0.03928) ? green / 12.92 : Math.pow(((green + 0.055) / 1.055), 2.4); + const B = (blue > 0.03928) ? blue / 12.92 : Math.pow(((blue + 0.055) / 1.055), 2.4); + + return 0.2126 * R + 0.7152 * G + 0.0722 * B; +} + + +/** + * Get a GdkRGBA contrasted for the input + * See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef + * + * @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color + * @return {Gdk.RGBA} A GdkRGBA object for the foreground color + */ +function getFgRGBA(rgba) { + const bgLuminance = relativeLuminance(rgba); + const lightContrast = (0.07275541795665634 + 0.05) / (bgLuminance + 0.05); + const darkContrast = (bgLuminance + 0.05) / (0.0046439628482972135 + 0.05); + + const value = (darkContrast > lightContrast) ? 0.06 : 0.94; + return new Gdk.RGBA({red: value, green: value, blue: value, alpha: 0.5}); +} + + +/** + * Get a GdkPixbuf for @path, allowing the corrupt JPEG's KDE Connect sometimes + * sends. This function is synchronous. + * + * @param {string} path - A local file path + * @param {number} size - Size in pixels + * @param {scale} [scale] - Scale factor for the size + * @return {Gdk.Pixbuf} A pixbuf + */ +function getPixbufForPath(path, size, scale = 1.0) { + let data, loader; + + // Catch missing avatar files + try { + data = GLib.file_get_contents(path)[1]; + } catch (e) { + debug(e, path); + return undefined; + } + + // Consider errors from partially corrupt JPEGs to be warnings + try { + loader = new GdkPixbuf.PixbufLoader(); + loader.write(data); + loader.close(); + } catch (e) { + debug(e, path); + } + + const pixbuf = loader.get_pixbuf(); + + // Scale to monitor + size = Math.floor(size * scale); + return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER); +} + +function getPixbufForIcon(name, size, scale, bgColor) { + const color = getFgRGBA(bgColor); + const theme = Gtk.IconTheme.get_default(); + const info = theme.lookup_icon_for_scale( + name, + size, + scale, + Gtk.IconLookupFlags.FORCE_SYMBOLIC + ); + + return info.load_symbolic(color, null, null, null)[0]; +} + + +/** + * Return a localized string for a phone number type + * See: http://www.ietf.org/rfc/rfc2426.txt + * + * @param {string} type - An RFC2426 phone number type + * @return {string} A localized string like 'Mobile' + */ +function getNumberTypeLabel(type) { + if (type.includes('fax')) + // TRANSLATORS: A fax number + return _('Fax'); + + if (type.includes('work')) + // TRANSLATORS: A work or office phone number + return _('Work'); + + if (type.includes('cell')) + // TRANSLATORS: A mobile or cellular phone number + return _('Mobile'); + + if (type.includes('home')) + // TRANSLATORS: A home phone number + return _('Home'); + + // TRANSLATORS: All other phone number types + return _('Other'); +} + +/** + * Get a display number from @contact for @address. + * + * @param {Object} contact - A contact object + * @param {string} address - A phone number + * @return {string} A (possibly) better display number for the address + */ +function getDisplayNumber(contact, address) { + const number = address.toPhoneNumber(); + + for (const contactNumber of contact.numbers) { + const cnumber = contactNumber.value.toPhoneNumber(); + + if (number.endsWith(cnumber) || cnumber.endsWith(number)) + return GLib.markup_escape_text(contactNumber.value, -1); + } + + return GLib.markup_escape_text(address, -1); +} + + +/** + * Contact Avatar + */ +const AvatarCache = new WeakMap(); + +var Avatar = GObject.registerClass({ + GTypeName: 'GSConnectContactAvatar', +}, class ContactAvatar extends Gtk.DrawingArea { + + _init(contact = null) { + super._init({ + height_request: 32, + width_request: 32, + valign: Gtk.Align.CENTER, + visible: true, + }); + + this.contact = contact; + } + + get rgba() { + if (this._rgba === undefined) { + if (this.contact) + this._rgba = randomRGBA(this.contact.name); + else + this._rgba = randomRGBA(GLib.uuid_string_random()); + } + + return this._rgba; + } + + get contact() { + if (this._contact === undefined) + this._contact = null; + + return this._contact; + } + + set contact(contact) { + if (this.contact === contact) + return; + + this._contact = contact; + this._surface = undefined; + this._rgba = undefined; + this._offset = 0; + } + + _loadSurface() { + // Get the monitor scale + const display = Gdk.Display.get_default(); + const monitor = display.get_monitor_at_window(this.get_window()); + const scale = monitor.get_scale_factor(); + + // If there's a contact with an avatar, try to load it + if (this.contact && this.contact.avatar) { + // Check the cache + this._surface = AvatarCache.get(this.contact); + + // Try loading the pixbuf + if (!this._surface) { + const pixbuf = getPixbufForPath( + this.contact.avatar, + this.width_request, + scale + ); + + if (pixbuf) { + this._surface = Gdk.cairo_surface_create_from_pixbuf( + pixbuf, + 0, + this.get_window() + ); + AvatarCache.set(this.contact, this._surface); + } + } + } + + // If we still don't have a surface, load a fallback + if (!this._surface) { + let iconName; + + // If we were given a contact, it's direct message otherwise group + if (this.contact) + iconName = 'avatar-default-symbolic'; + else + iconName = 'group-avatar-symbolic'; + + // Center the icon + this._offset = (this.width_request - 24) / 2; + + // Load the fallback + const pixbuf = getPixbufForIcon(iconName, 24, scale, this.rgba); + + this._surface = Gdk.cairo_surface_create_from_pixbuf( + pixbuf, + 0, + this.get_window() + ); + } + } + + vfunc_draw(cr) { + if (!this._surface) + this._loadSurface(); + + // Clip to a circle + const rad = this.width_request / 2; + cr.arc(rad, rad, rad, 0, 2 * Math.PI); + cr.clipPreserve(); + + // Fill the background if the the surface is offset + if (this._offset > 0) { + Gdk.cairo_set_source_rgba(cr, this.rgba); + cr.fill(); + } + + // Draw the avatar/icon + cr.setSourceSurface(this._surface, this._offset, this._offset); + cr.paint(); + + cr.$dispose(); + return Gdk.EVENT_PROPAGATE; + } +}); + + +/** + * A row for a contact address (usually a phone number). + */ +const AddressRow = GObject.registerClass({ + GTypeName: 'GSConnectContactsAddressRow', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contacts-address-row.ui', + Children: ['avatar', 'name-label', 'address-label', 'type-label'], +}, class AddressRow extends Gtk.ListBoxRow { + + _init(contact, index = 0) { + super._init(); + + this._index = index; + this._number = contact.numbers[index]; + this.contact = contact; + } + + get contact() { + if (this._contact === undefined) + this._contact = null; + + return this._contact; + } + + set contact(contact) { + if (this.contact === contact) + return; + + this._contact = contact; + + if (this._index === 0) { + this.avatar.contact = contact; + this.avatar.visible = true; + + this.name_label.label = GLib.markup_escape_text(contact.name, -1); + this.name_label.visible = true; + + this.address_label.margin_start = 0; + this.address_label.margin_end = 0; + } else { + this.avatar.visible = false; + this.name_label.visible = false; + + // TODO: rtl inverts margin-start so the number don't align + this.address_label.margin_start = 38; + this.address_label.margin_end = 38; + } + + this.address_label.label = GLib.markup_escape_text(this.number.value, -1); + + if (this.number.type !== undefined) + this.type_label.label = getNumberTypeLabel(this.number.type); + } + + get number() { + if (this._number === undefined) + return {value: 'unknown', type: 'unknown'}; + + return this._number; + } +}); + + +/** + * A widget for selecting contact addresses (usually phone numbers) + */ +var ContactChooser = GObject.registerClass({ + GTypeName: 'GSConnectContactChooser', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'store': GObject.ParamSpec.object( + 'store', + 'Store', + 'The contacts store', + GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT, + GObject.Object + ), + }, + Signals: { + 'number-selected': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [GObject.TYPE_STRING], + }, + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/contact-chooser.ui', + Children: ['entry', 'list', 'scrolled'], +}, class ContactChooser extends Gtk.Grid { + + _init(params) { + super._init(params); + + // Setup the contact list + this.list._entry = this.entry.text; + this.list.set_filter_func(this._filter); + this.list.set_sort_func(this._sort); + + // Make sure we're using the correct contacts store + this.device.bind_property( + 'contacts', + this, + 'store', + GObject.BindingFlags.SYNC_CREATE + ); + + // Cleanup on ::destroy + this.connect('destroy', this._onDestroy); + } + + get store() { + if (this._store === undefined) + this._store = null; + + return this._store; + } + + set store(store) { + if (this.store === store) + return; + + // Unbind the old store + if (this._store) { + // Disconnect from the store + this._store.disconnect(this._contactAddedId); + this._store.disconnect(this._contactRemovedId); + this._store.disconnect(this._contactChangedId); + + // Clear the contact list + const rows = this.list.get_children(); + + for (let i = 0, len = rows.length; i < len; i++) { + rows[i].destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + + // Set the store + this._store = store; + + // Bind the new store + if (this._store) { + // Connect to the new store + this._contactAddedId = store.connect( + 'contact-added', + this._onContactAdded.bind(this) + ); + + this._contactRemovedId = store.connect( + 'contact-removed', + this._onContactRemoved.bind(this) + ); + + this._contactChangedId = store.connect( + 'contact-changed', + this._onContactChanged.bind(this) + ); + + // Populate the list + this._populate(); + } + } + + /* + * ContactStore Callbacks + */ + _onContactAdded(store, id) { + const contact = this.store.get_contact(id); + this._addContact(contact); + } + + _onContactRemoved(store, id) { + const rows = this.list.get_children(); + + for (let i = 0, len = rows.length; i < len; i++) { + const row = rows[i]; + + if (row.contact.id === id) { + row.destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + } + + _onContactChanged(store, id) { + this._onContactRemoved(store, id); + this._onContactAdded(store, id); + } + + _onDestroy(chooser) { + chooser.store = null; + } + + _onSearchChanged(entry) { + this.list._entry = entry.text; + let dynamic = this.list.get_row_at_index(0); + + // If the entry contains string with 2 or more digits... + if (entry.text.replace(/\D/g, '').length >= 2) { + // ...ensure we have a dynamic contact for it + if (!dynamic || !dynamic.__tmp) { + dynamic = new AddressRow({ + // TRANSLATORS: A phone number (eg. "Send to 555-5555") + name: _('Send to %s').format(entry.text), + numbers: [{type: 'unknown', value: entry.text}], + }); + dynamic.__tmp = true; + this.list.add(dynamic); + + // ...or if we already do, then update it + } else { + const address = entry.text; + + // Update contact object + dynamic.contact.name = address; + dynamic.contact.numbers[0].value = address; + + // Update UI + dynamic.name_label.label = _('Send to %s').format(address); + dynamic.address_label.label = address; + } + + // ...otherwise remove any dynamic contact that's been created + } else if (dynamic && dynamic.__tmp) { + dynamic.destroy(); + } + + this.list.invalidate_filter(); + this.list.invalidate_sort(); + } + + // GtkListBox::row-activated + _onNumberSelected(box, row) { + if (row === null) + return; + + // Emit the number + const address = row.number.value; + this.emit('number-selected', address); + + // Reset the contact list + this.entry.text = ''; + this.list.select_row(null); + this.scrolled.vadjustment.value = 0; + } + + _filter(row) { + // Dynamic contact always shown + if (row.__tmp) + return true; + + const query = row.get_parent()._entry; + + // Show contact if text is substring of name + const queryName = query.toLocaleLowerCase(); + + if (row.contact.name.toLocaleLowerCase().includes(queryName)) + return true; + + // Show contact if text is substring of number + const queryNumber = query.toPhoneNumber(); + + if (queryNumber.length) { + for (const number of row.contact.numbers) { + if (number.value.toPhoneNumber().includes(queryNumber)) + return true; + } + + // Query is effectively empty + } else if (/^0+/.test(query)) { + return true; + } + + return false; + } + + _sort(row1, row2) { + if (row1.__tmp) + return -1; + + if (row2.__tmp) + return 1; + + return row1.contact.name.localeCompare(row2.contact.name); + } + + _populate() { + // Add each contact + const contacts = this.store.contacts; + + for (let i = 0, len = contacts.length; i < len; i++) + this._addContact(contacts[i]); + } + + _addContactNumber(contact, index) { + const row = new AddressRow(contact, index); + this.list.add(row); + + return row; + } + + _addContact(contact) { + try { + // HACK: fix missing contact names + if (contact.name === undefined) + contact.name = _('Unknown Contact'); + + if (contact.numbers.length === 1) + return this._addContactNumber(contact, 0); + + for (let i = 0, len = contact.numbers.length; i < len; i++) + this._addContactNumber(contact, i); + } catch (e) { + logError(e); + } + } + + /** + * Get a dictionary of number-contact pairs for each selected phone number. + * + * @return {Object[]} A dictionary of contacts + */ + getSelected() { + try { + const selected = {}; + + for (const row of this.list.get_selected_rows()) + selected[row.number.value] = row.contact; + + return selected; + } catch (e) { + logError(e); + return {}; + } + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js new file mode 100644 index 0000000..dd4fde6 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/legacyMessaging.js @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Contacts = imports.service.ui.contacts; +const Messaging = imports.service.ui.messaging; +const URI = imports.service.utils.uri; +const _ui = imports.service.utils.ui; + + +var Dialog = GObject.registerClass({ + GTypeName: 'GSConnectLegacyMessagingDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/legacy-messaging-dialog.ui', + Children: [ + 'infobar', 'stack', + 'message-box', 'message-avatar', 'message-label', 'entry', + ], +}, class Dialog extends Gtk.Dialog { + + _init(params) { + super._init({ + application: Gio.Application.get_default(), + device: params.device, + plugin: params.plugin, + use_header_bar: true, + }); + + this.set_response_sensitive(Gtk.ResponseType.OK, false); + + // Dup some functions + this.headerbar = this.get_titlebar(); + this._setHeaderBar = Messaging.Window.prototype._setHeaderBar; + + // Info bar + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Message Entry/Send Button + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.DEFAULT + ); + + this._connectedId = this.device.connect( + 'notify::connected', + this._onStateChanged.bind(this) + ); + + this._entryChangedId = this.entry.buffer.connect( + 'changed', + this._onStateChanged.bind(this) + ); + + // Set the message if given + if (params.message) { + this.message = params.message; + this.addresses = params.message.addresses; + + this.message_avatar.contact = this.device.contacts.query({ + number: this.addresses[0].address, + }); + this.message_label.label = URI.linkify(this.message.body); + this.message_box.visible = true; + + // Otherwise set the address(es) if we were passed those + } else if (params.addresses) { + this.addresses = params.addresses; + } + + // Load the contact list if we weren't supplied with an address + if (this.addresses.length === 0) { + this.contact_chooser = new Contacts.ContactChooser({ + device: this.device, + }); + this.stack.add_named(this.contact_chooser, 'contact-chooser'); + this.stack.child_set_property(this.contact_chooser, 'position', 0); + + this._numberSelectedId = this.contact_chooser.connect( + 'number-selected', + this._onNumberSelected.bind(this) + ); + + this.stack.visible_child_name = 'contact-chooser'; + } + + this.restoreGeometry('legacy-messaging-dialog'); + + this.connect('destroy', this._onDestroy); + } + + _onDestroy(dialog) { + if (dialog._numberSelectedId !== undefined) { + dialog.contact_chooser.disconnect(dialog._numberSelectedId); + dialog.contact_chooser.destroy(); + } + + dialog.entry.buffer.disconnect(dialog._entryChangedId); + dialog.device.disconnect(dialog._connectedId); + } + + vfunc_delete_event() { + this.saveGeometry(); + + return false; + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + // Refuse to send empty or whitespace only texts + if (!this.entry.buffer.text.trim()) + return; + + this.plugin.sendMessage( + this.addresses, + this.entry.buffer.text, + 1, + true + ); + } + + this.destroy(); + } + + get addresses() { + if (this._addresses === undefined) + this._addresses = []; + + return this._addresses; + } + + set addresses(addresses = []) { + this._addresses = addresses; + + // Set the headerbar + this._setHeaderBar(this._addresses); + + // Show the message editor + this.stack.visible_child_name = 'message-editor'; + this._onStateChanged(); + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + _onNumberSelected(chooser, number) { + const contacts = chooser.getSelected(); + + this.addresses = Object.keys(contacts).map(address => { + return {address: address}; + }); + } + + _onStateChanged() { + if (this.device.connected && + this.entry.buffer.text.trim() && + this.stack.visible_child_name === 'message-editor') + this.set_response_sensitive(Gtk.ResponseType.OK, true); + else + this.set_response_sensitive(Gtk.ResponseType.OK, false); + } + + /** + * Set the contents of the message entry + * + * @param {string} text - The message to place in the entry + */ + setMessage(text) { + this.entry.buffer.text = text; + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js new file mode 100644 index 0000000..60c0376 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/messaging.js @@ -0,0 +1,1320 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Tweener = imports.tweener.tweener; + +const Gdk = imports.gi.Gdk; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; + +const Contacts = imports.service.ui.contacts; +const Sms = imports.service.plugins.sms; +const URI = imports.service.utils.uri; +const _ui = imports.service.utils.ui; + + +/* + * Useful time constants + */ +const TIME_SPAN_MINUTE = 60000; +const TIME_SPAN_HOUR = 3600000; +const TIME_SPAN_DAY = 86400000; +const TIME_SPAN_WEEK = 604800000; + + +// Less than an hour (eg. 42 minutes ago) +const _lthLong = new Intl.RelativeTimeFormat('default', { + numeric: 'auto', + style: 'long', +}); + +// Less than a day ago (eg. 11:42 PM) +const _ltdFormat = new Intl.DateTimeFormat('default', { + hour: 'numeric', + minute: 'numeric', +}); + +// Less than a week ago (eg. Monday) +const _ltwLong = new Intl.DateTimeFormat('default', { + weekday: 'long', +}); + +// Less than a week ago (eg. Mon) +const _ltwShort = new Intl.DateTimeFormat('default', { + weekday: 'short', +}); + +// Less than a year (eg. Oct 31) +const _ltyShort = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'short', +}); + +// Less than a year (eg. October 31) +const _ltyLong = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'long', +}); + +// Greater than a year (eg. October 31, 2019) +const _gtyLong = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'long', + year: 'numeric', +}); + +// Greater than a year (eg. 10/31/2019) +const _gtyShort = new Intl.DateTimeFormat('default', { + day: 'numeric', + month: 'numeric', + year: 'numeric', +}); + +// Pretty close to strftime's %c +const _cFormat = new Intl.DateTimeFormat('default', { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', +}); + + +/** + * Return a human-readable timestamp, formatted for longer contexts. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp similar to what Android Messages uses + */ +function getTime(time) { + const date = new Date(time); + const now = new Date(); + const diff = now - time; + + // Super recent + if (diff < TIME_SPAN_MINUTE) + // TRANSLATORS: Less than a minute ago + return _('Just now'); + + // Under an hour (TODO: these labels aren't updated) + if (diff < TIME_SPAN_HOUR) + return _lthLong.format(-Math.floor(diff / TIME_SPAN_MINUTE), 'minute'); + + // Yesterday, but less than 24 hours ago + if (diff < TIME_SPAN_DAY && now.getDay() !== date.getDay()) + // TRANSLATORS: Yesterday, but less than 24 hours (eg. Yesterday · 11:29 PM) + return _('Yesterday・%s').format(_ltdFormat.format(time)); + + // Less than a day ago + if (diff < TIME_SPAN_DAY) + return _ltdFormat.format(time); + + // Less than a week ago + if (diff < TIME_SPAN_WEEK) + return _ltwLong.format(time); + + // Sometime this year + if (date.getFullYear() === now.getFullYear()) + return _ltyLong.format(time); + + // Earlier than that + return _gtyLong.format(time); +} + + +/** + * Return a human-readable timestamp, formatted for shorter contexts. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp similar to what Android Messages uses + */ +function getShortTime(time) { + const date = new Date(time); + const now = new Date(); + const diff = now - time; + + if (diff < TIME_SPAN_MINUTE) + // TRANSLATORS: Less than a minute ago + return _('Just now'); + + if (diff < TIME_SPAN_HOUR) { + // TRANSLATORS: Time duration in minutes (eg. 15 minutes) + return ngettext( + '%d minute', + '%d minutes', + (diff / TIME_SPAN_MINUTE) + ).format(diff / TIME_SPAN_MINUTE); + } + + // Less than a day ago + if (diff < TIME_SPAN_DAY) + return _ltdFormat.format(time); + + // Less than a week ago + if (diff < TIME_SPAN_WEEK) + return _ltwShort.format(time); + + // Sometime this year + if (date.getFullYear() === now.getFullYear()) + return _ltyShort.format(time); + + // Earlier than that + return _gtyShort.format(time); +} + + +/** + * Return a human-readable timestamp, similar to `strftime()` with `%c`. + * + * @param {number} time - Milliseconds since the epoch (local time) + * @return {string} A localized timestamp + */ +function getDetailedTime(time) { + return _cFormat.format(time); +} + + +function setAvatarVisible(row, visible) { + const incoming = row.message.type === Sms.MessageBox.INBOX; + + // Adjust the margins + if (visible) { + row.grid.margin_start = incoming ? 6 : 56; + row.grid.margin_bottom = 6; + } else { + row.grid.margin_start = incoming ? 44 : 56; + row.grid.margin_bottom = 0; + } + + // Show hide the avatar + if (incoming) + row.avatar.visible = visible; +} + + +/** + * A ListBoxRow for a preview of a conversation + */ +const ConversationMessage = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversationMessage', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation-message.ui', + Children: ['grid', 'avatar', 'sender-label', 'message-label'], +}, class ConversationMessage extends Gtk.ListBoxRow { + _init(contact, message) { + super._init(); + + this.contact = contact; + this.message = message; + + // Sort properties + this.sender = message.addresses[0].address || 'unknown'; + this.message_label.label = URI.linkify(message.body); + this.message_label.tooltip_text = getDetailedTime(message.date); + + // Add avatar for incoming messages + if (message.type === Sms.MessageBox.INBOX) { + this.grid.margin_end = 18; + this.grid.halign = Gtk.Align.START; + + this.avatar.contact = this.contact; + this.avatar.visible = true; + + this.sender_label.label = contact.name; + this.sender_label.visible = true; + + this.message_label.get_style_context().add_class('message-in'); + this.message_label.halign = Gtk.Align.START; + } else { + this.message_label.get_style_context().add_class('message-out'); + } + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + get date() { + return this._message.date; + } + + get thread_id() { + return this._message.thread_id; + } + + get message() { + if (this._message === undefined) + this._message = null; + + return this._message; + } + + set message(message) { + this._message = message; + } +}); + + +/** + * A widget for displaying a conversation thread, with an entry for responding. + */ +const Conversation = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversation', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this conversation', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing this conversation', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'has-pending': GObject.ParamSpec.boolean( + 'has-pending', + 'Has Pending', + 'Whether there are sent messages pending confirmation', + GObject.ParamFlags.READABLE, + false + ), + 'thread-id': GObject.ParamSpec.string( + 'thread-id', + 'Thread ID', + 'The current thread', + GObject.ParamFlags.READWRITE, + '' + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation.ui', + Children: [ + 'entry', 'list', 'scrolled', + 'pending', 'pending-box', + ], +}, class MessagingConversation extends Gtk.Grid { + + _init(params) { + super._init({ + device: params.device, + plugin: params.plugin, + }); + Object.assign(this, params); + + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.SYNC_CREATE + ); + + // If we're disconnected pending messages might not succeed, but we'll + // leave them until reconnect when we'll ask for an update + this._connectedId = this.device.connect( + 'notify::connected', + this._onConnected.bind(this) + ); + + // Pending messages + this.pending.message = { + date: Number.MAX_SAFE_INTEGER, + type: Sms.MessageBox.OUTBOX, + }; + + // Auto-scrolling + this._vadj = this.scrolled.get_vadjustment(); + this._scrolledId = this._vadj.connect( + 'value-changed', + this._holdPosition.bind(this) + ); + + // Message List + this.list.set_header_func(this._headerMessages); + this.list.set_sort_func(this._sortMessages); + this._populateMessages(); + + // Cleanup on ::destroy + this.connect('destroy', this._onDestroy); + } + + get addresses() { + if (this._addresses === undefined) + this._addresses = []; + + return this._addresses; + } + + set addresses(addresses) { + if (!addresses || addresses.length === 0) { + this._addresses = []; + this._contacts = {}; + return; + } + + // Lookup a contact for each address object, then loop back to correct + // each address carried by the message. + this._addresses = addresses; + + for (let i = 0, len = this.addresses.length; i < len; i++) { + // Lookup the contact + const address = this.addresses[i].address; + const contact = this.device.contacts.query({number: address}); + + // Get corrected address + let number = address.toPhoneNumber(); + + if (!number) + continue; + + for (const contactNumber of contact.numbers) { + const cnumber = contactNumber.value.toPhoneNumber(); + + if (cnumber && (number.endsWith(cnumber) || cnumber.endsWith(number))) { + number = contactNumber.value; + break; + } + } + + // Store the final result + this.addresses[i].address = number; + this.contacts[address] = contact; + } + + // TODO: Mark the entry as insensitive for group messages + if (this.addresses.length > 1) { + this.entry.placeholder_text = _('Not available'); + this.entry.secondary_icon_name = null; + this.entry.secondary_icon_tooltip_text = null; + this.entry.sensitive = false; + this.entry.tooltip_text = null; + } + } + + get contacts() { + if (this._contacts === undefined) + this._contacts = {}; + + return this._contacts; + } + + get has_pending() { + if (this.pending_box === undefined) + return false; + + return (this.pending_box.get_children().length > 0); + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get thread_id() { + if (this._thread_id === undefined) + this._thread_id = null; + + return this._thread_id; + } + + set thread_id(thread_id) { + const thread = this.plugin.threads[thread_id]; + const message = (thread) ? thread[0] : null; + + if (message && this.addresses.length === 0) { + this.addresses = message.addresses; + this._thread_id = thread_id; + } + } + + _onConnected(device) { + if (device.connected) + this.pending_box.foreach(msg => msg.destroy()); + } + + _onDestroy(conversation) { + conversation.device.disconnect(conversation._connectedId); + conversation._vadj.disconnect(conversation._scrolledId); + + conversation.list.foreach(message => { + // HACK: temporary mitigator for mysterious GtkListBox leak + message.destroy(); + imports.system.gc(); + }); + } + + _onEdgeReached(scrolled_window, pos) { + // Try to load more messages + if (pos === Gtk.PositionType.TOP) + this.logPrevious(); + + // Release any hold to resume auto-scrolling + else if (pos === Gtk.PositionType.BOTTOM) + this._releasePosition(); + } + + _onEntryChanged(entry) { + entry.secondary_icon_sensitive = (entry.text.length); + } + + _onKeyPressEvent(entry, event) { + const keyval = event.get_keyval()[1]; + const state = event.get_state()[1]; + const mask = state & Gtk.accelerator_get_default_mod_mask(); + + if (keyval === Gdk.KEY_Return && (mask & Gdk.ModifierType.SHIFT_MASK)) { + entry.emit('insert-at-cursor', '\n'); + return true; + } + + return false; + } + + _onSendMessage(entry, signal_id, event) { + // Don't send empty texts + if (!this.entry.text.trim()) + return; + + // Send the message + this.plugin.sendMessage(this.addresses, this.entry.text); + + // Add a phony message in the pending box + const message = new Gtk.Label({ + label: URI.linkify(this.entry.text), + halign: Gtk.Align.END, + selectable: true, + use_markup: true, + visible: true, + wrap: true, + wrap_mode: Pango.WrapMode.WORD_CHAR, + xalign: 0, + }); + message.get_style_context().add_class('message-out'); + message.date = Date.now(); + message.type = Sms.MessageBox.SENT; + + // Notify to reveal the pending box + this.pending_box.add(message); + this.notify('has-pending'); + + // Clear the entry + this.entry.text = ''; + } + + _onSizeAllocate(listbox, allocation) { + const upper = this._vadj.get_upper(); + const pageSize = this._vadj.get_page_size(); + + // If the scrolled window hasn't been filled yet, load another message + if (upper <= pageSize) { + this.logPrevious(); + this.scrolled.get_child().check_resize(); + + // We've been asked to hold the position, so we'll reset the adjustment + // value and update the hold position + } else if (this.__pos) { + this._vadj.set_value(upper - this.__pos); + + // Otherwise we probably appended a message and should scroll to it + } else { + this._scrollPosition(Gtk.PositionType.BOTTOM); + } + } + + /** + * Create a message row, ensuring a contact object has been retrieved or + * generated for the message. + * + * @param {Object} message - A dictionary of message data + * @return {ConversationMessage} A message row + */ + _createMessageRow(message) { + // Ensure we have a contact + const sender = message.addresses[0].address || 'unknown'; + + if (this.contacts[sender] === undefined) { + this.contacts[sender] = this.device.contacts.query({ + number: sender, + }); + } + + return new ConversationMessage(this.contacts[sender], message); + } + + _populateMessages() { + this.__first = null; + this.__last = null; + this.__pos = 0; + this.__messages = []; + + // Try and find a thread_id for this number + if (this.thread_id === null && this.addresses.length) + this._thread_id = this.plugin.getThreadIdForAddresses(this.addresses); + + // Make a copy of the thread and fill the window with messages + if (this.plugin.threads[this.thread_id]) { + this.__messages = this.plugin.threads[this.thread_id].slice(0); + this.logPrevious(); + } + } + + _headerMessages(row, before) { + // Skip pending + if (row.get_name() === 'pending') + return; + + if (before === null) + return setAvatarVisible(row, true); + + // Add date header if the last message was more than an hour ago + let header = row.get_header(); + + if ((row.message.date - before.message.date) > TIME_SPAN_HOUR) { + if (!header) { + header = new Gtk.Label({visible: true, selectable: true}); + header.get_style_context().add_class('dim-label'); + row.set_header(header); + } + + header.label = getTime(row.message.date); + + // Also show the avatar + setAvatarVisible(row, true); + + row.sender_label.visible = row.message.addresses.length > 1; + + // Or if the previous sender was the same, hide its avatar + } else if (row.message.type === before.message.type && + row.sender.equalsPhoneNumber(before.sender)) { + setAvatarVisible(before, false); + setAvatarVisible(row, true); + + row.sender_label.visible = false; + + // otherwise show the avatar + } else { + setAvatarVisible(row, true); + } + } + + _holdPosition() { + this.__pos = this._vadj.get_upper() - this._vadj.get_value(); + } + + _releasePosition() { + this.__pos = 0; + } + + _scrollPosition(pos = Gtk.PositionType.BOTTOM, animate = true) { + let vpos = pos; + this._vadj.freeze_notify(); + + if (pos === Gtk.PositionType.BOTTOM) + vpos = this._vadj.get_upper() - this._vadj.get_page_size(); + + if (animate) { + Tweener.addTween(this._vadj, { + value: vpos, + time: 0.5, + transition: 'easeInOutCubic', + onComplete: () => this._vadj.thaw_notify(), + }); + } else { + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this._vadj.set_value(vpos); + this._vadj.thaw_notify(); + }); + } + } + + _sortMessages(row1, row2) { + return (row1.message.date > row2.message.date) ? 1 : -1; + } + + /** + * Log the next message in the conversation. + * + * @param {Object} message - A message object + */ + logNext(message) { + try { + // TODO: Unsupported MessageBox + if (message.type !== Sms.MessageBox.INBOX && + message.type !== Sms.MessageBox.SENT) + throw TypeError(`invalid message box ${message.type}`); + + // Append the message + const row = this._createMessageRow(message); + this.list.add(row); + this.list.invalidate_headers(); + + // Remove the first pending message + if (this.has_pending && message.type === Sms.MessageBox.SENT) { + this.pending_box.get_children()[0].destroy(); + this.notify('has-pending'); + } + } catch (e) { + debug(e); + } + } + + /** + * Log the previous message in the thread + */ + logPrevious() { + try { + const message = this.__messages.pop(); + + if (!message) + return; + + // TODO: Unsupported MessageBox + if (message.type !== Sms.MessageBox.INBOX && + message.type !== Sms.MessageBox.SENT) + throw TypeError(`invalid message box ${message.type}`); + + // Prepend the message + const row = this._createMessageRow(message); + this.list.prepend(row); + this.list.invalidate_headers(); + } catch (e) { + debug(e); + } + } + + /** + * Set the contents of the message entry + * + * @param {string} text - The message to place in the entry + */ + setMessage(text) { + this.entry.text = text; + this.entry.emit('move-cursor', 0, text.length, false); + } +}); + + +/** + * A ListBoxRow for a preview of a conversation + */ +const ConversationSummary = GObject.registerClass({ + GTypeName: 'GSConnectMessagingConversationSummary', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-conversation-summary.ui', + Children: ['avatar', 'name-label', 'time-label', 'body-label'], +}, class ConversationSummary extends Gtk.ListBoxRow { + _init(contacts, message) { + super._init(); + + this.contacts = contacts; + this.message = message; + } + + get date() { + return this._message.date; + } + + get thread_id() { + return this._message.thread_id; + } + + get message() { + return this._message; + } + + set message(message) { + this._message = message; + this._sender = message.addresses[0].address || 'unknown'; + + // Contact Name + let nameLabel = _('Unknown Contact'); + + // Update avatar for single-recipient messages + if (message.addresses.length === 1) { + this.avatar.contact = this.contacts[this._sender]; + nameLabel = GLib.markup_escape_text(this.avatar.contact.name, -1); + } else { + this.avatar.contact = null; + nameLabel = _('Group Message'); + const participants = []; + message.addresses.forEach((address) => { + participants.push(this.contacts[address.address].name); + }); + this.name_label.tooltip_text = participants.join(', '); + } + + // Contact Name & Message body + let bodyLabel = message.body.split(/\r|\n/)[0]; + bodyLabel = GLib.markup_escape_text(bodyLabel, -1); + + // Ignore the 'read' flag if it's an outgoing message + if (message.type === Sms.MessageBox.SENT) { + // TRANSLATORS: An outgoing message body in a conversation summary + bodyLabel = _('You: %s').format(bodyLabel); + + // Otherwise make it bold if it's unread + } else if (message.read === Sms.MessageStatus.UNREAD) { + nameLabel = `<b>${nameLabel}</b>`; + bodyLabel = `<b>${bodyLabel}</b>`; + } + + // Set the labels, body always smaller + this.name_label.label = nameLabel; + this.body_label.label = `<small>${bodyLabel}</small>`; + + // Time + const timeLabel = `<small>${getShortTime(message.date)}</small>`; + this.time_label.label = timeLabel; + } + + /** + * Update the relative time label. + */ + update() { + const timeLabel = `<small>${getShortTime(this.message.date)}</small>`; + this.time_label.label = timeLabel; + } +}); + + +/** + * A Gtk.ApplicationWindow for SMS conversations + */ +var Window = GObject.registerClass({ + GTypeName: 'GSConnectMessagingWindow', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'thread-id': GObject.ParamSpec.string( + 'thread-id', + 'Thread ID', + 'The current thread', + GObject.ParamFlags.READWRITE, + '' + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/messaging-window.ui', + Children: [ + 'headerbar', 'infobar', + 'thread-list', 'stack', + ], +}, class MessagingWindow extends Gtk.ApplicationWindow { + + _init(params) { + super._init(params); + this.headerbar.subtitle = this.device.name; + + this.insert_action_group('device', this.device); + + // Device Status + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Contacts + this.contact_chooser = new Contacts.ContactChooser({ + device: this.device, + }); + this.stack.add_named(this.contact_chooser, 'contact-chooser'); + + this._numberSelectedId = this.contact_chooser.connect( + 'number-selected', + this._onNumberSelected.bind(this) + ); + + // Threads + this.thread_list.set_sort_func(this._sortThreads); + + this._threadsChangedId = this.plugin.connect( + 'notify::threads', + this._onThreadsChanged.bind(this) + ); + + this._timestampThreadsId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT_IDLE, + 60, + this._timestampThreads.bind(this) + ); + + this._sync(); + this._onThreadsChanged(); + this.restoreGeometry('messaging'); + } + + vfunc_delete_event(event) { + this.saveGeometry(); + + GLib.source_remove(this._timestampThreadsId); + this.contact_chooser.disconnect(this._numberSelectedId); + this.plugin.disconnect(this._threadsChangedId); + + return false; + } + + get plugin() { + return this._plugin || null; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get thread_id() { + return this.stack.visible_child_name; + } + + set thread_id(thread_id) { + thread_id = `${thread_id}`; // FIXME + + // Reset to the empty placeholder + if (!thread_id) { + this.thread_list.select_row(null); + this.stack.set_visible_child_name('placeholder'); + return; + } + + // Create a conversation widget if there isn't one + let conversation = this.stack.get_child_by_name(thread_id); + const thread = this.plugin.threads[thread_id]; + + if (conversation === null) { + if (!thread) { + debug(`Thread ID ${thread_id} not found`); + return; + } + + conversation = new Conversation({ + device: this.device, + plugin: this.plugin, + thread_id: thread_id, + }); + + this.stack.add_named(conversation, thread_id); + } + + // Figure out whether this is a multi-recipient thread + this._setHeaderBar(thread[0].addresses); + + // Select the conversation and entry active + this.stack.visible_child = conversation; + this.stack.visible_child.entry.has_focus = true; + + // There was a pending message waiting for a conversation to be chosen + if (this._pendingShare) { + conversation.setMessage(this._pendingShare); + this._pendingShare = null; + } + + this._thread_id = thread_id; + this.notify('thread_id'); + } + + _setHeaderBar(addresses = []) { + const address = addresses[0].address; + const contact = this.device.contacts.query({number: address}); + + if (addresses.length === 1) { + this.headerbar.title = contact.name; + this.headerbar.subtitle = Contacts.getDisplayNumber(contact, address); + } else { + const otherLength = addresses.length - 1; + + this.headerbar.title = contact.name; + this.headerbar.subtitle = ngettext( + 'And %d other contact', + 'And %d others', + otherLength + ).format(otherLength); + } + } + + _sync() { + this.device.contacts.fetch(); + this.plugin.connected(); + } + + _onNewConversation() { + this._sync(); + this.stack.set_visible_child_name('contact-chooser'); + this.thread_list.select_row(null); + this.contact_chooser.entry.has_focus = true; + } + + _onNumberSelected(chooser, number) { + const contacts = chooser.getSelected(); + const row = this._getRowForContacts(contacts); + + if (row) + row.emit('activate'); + else + this.setContacts(contacts); + } + + /** + * Threads + */ + _onThreadsChanged() { + // Get the last message in each thread + const messages = {}; + + for (const [thread_id, thread] of Object.entries(this.plugin.threads)) { + const message = thread[thread.length - 1]; + + // Skip messages without a body (eg. MMS messages without text) + if (message.body) + messages[thread_id] = thread[thread.length - 1]; + } + + // Update existing summaries and destroy old ones + for (const row of this.thread_list.get_children()) { + const message = messages[row.thread_id]; + + // If it's an existing conversation, update it + if (message) { + // Ensure there's a contact mapping + const sender = message.addresses[0].address || 'unknown'; + + if (row.contacts[sender] === undefined) { + row.contacts[sender] = this.device.contacts.query({ + number: sender, + }); + } + + row.message = message; + delete messages[row.thread_id]; + + // Otherwise destroy it + } else { + // Destroy the conversation widget + const conversation = this.stack.get_child_by_name(`${row.thread_id}`); + + if (conversation) { + conversation.destroy(); + imports.system.gc(); + } + + // Then the summary widget + row.destroy(); + // HACK: temporary mitigator for mysterious GtkListBox leak + imports.system.gc(); + } + } + + // What's left in the dictionary is new summaries + for (const message of Object.values(messages)) { + const contacts = this.device.contacts.lookupAddresses(message.addresses); + const conversation = new ConversationSummary(contacts, message); + this.thread_list.add(conversation); + } + + // Re-sort the summaries + this.thread_list.invalidate_sort(); + } + + // GtkListBox::row-activated + _onThreadSelected(box, row) { + // Show the conversation for this number (if applicable) + if (row) { + this.thread_id = row.thread_id; + + // Show the placeholder + } else { + this.headerbar.title = _('Messaging'); + this.headerbar.subtitle = this.device.name; + } + } + + _sortThreads(row1, row2) { + return (row1.date > row2.date) ? -1 : 1; + } + + _timestampThreads() { + if (this.visible) + this.thread_list.foreach(row => row.update()); + + return GLib.SOURCE_CONTINUE; + } + + /** + * Find the thread row for @contacts + * + * @param {Object[]} contacts - A contact group + * @return {ConversationSummary|null} The thread row or %null + */ + _getRowForContacts(contacts) { + const addresses = Object.keys(contacts).map(address => { + return {address: address}; + }); + + // Try to find a thread_id + const thread_id = this.plugin.getThreadIdForAddresses(addresses); + + for (const row of this.thread_list.get_children()) { + if (row.message.thread_id === thread_id) + return row; + } + + return null; + } + + setContacts(contacts) { + // Group the addresses + const addresses = []; + + for (const address of Object.keys(contacts)) + addresses.push({address: address}); + + // Try to find a thread ID for this address group + let thread_id = this.plugin.getThreadIdForAddresses(addresses); + + if (thread_id === null) + thread_id = GLib.uuid_string_random(); + else + thread_id = thread_id.toString(); + + // Try to find a thread row for the ID + const row = this._getRowForContacts(contacts); + + if (row !== null) { + this.thread_list.select_row(row); + return; + } + + // We're creating a new conversation + const conversation = new Conversation({ + device: this.device, + plugin: this.plugin, + addresses: addresses, + }); + + // Set the headerbar + this._setHeaderBar(addresses); + + // Select the conversation and entry active + this.stack.add_named(conversation, thread_id); + this.stack.visible_child = conversation; + this.stack.visible_child.entry.has_focus = true; + + // There was a pending message waiting for a conversation to be chosen + if (this._pendingShare) { + conversation.setMessage(this._pendingShare); + this._pendingShare = null; + } + + this._thread_id = thread_id; + this.notify('thread-id'); + } + + _includesAddress(addresses, addressObj) { + const number = addressObj.address.toPhoneNumber(); + + for (const haystackObj of addresses) { + const tnumber = haystackObj.address.toPhoneNumber(); + + if (number.endsWith(tnumber) || tnumber.endsWith(number)) + return true; + } + + return false; + } + + /** + * Try and find an existing conversation widget for @message. + * + * @param {Object} message - A message object + * @return {Conversation|null} A conversation widget or %null + */ + getConversationForMessage(message) { + // TODO: This shouldn't happen? + if (message === null) + return null; + + // First try to find a conversation by thread_id + const thread_id = `${message.thread_id}`; + const conversation = this.stack.get_child_by_name(thread_id); + + if (conversation !== null) + return conversation; + + // Try and find one by matching addresses, which is necessary if we've + // started a thread locally and haven't set the thread_id + const addresses = message.addresses; + + for (const conversation of this.stack.get_children()) { + if (conversation.addresses === undefined || + conversation.addresses.length !== addresses.length) + continue; + + const caddrs = conversation.addresses; + + // If we find a match, set `thread-id` on the conversation and the + // child property `name`. + if (addresses.every(addr => this._includesAddress(caddrs, addr))) { + conversation._thread_id = thread_id; + this.stack.child_set_property(conversation, 'name', thread_id); + + return conversation; + } + } + + return null; + } + + /** + * Set the contents of the message entry. If @pending is %false set the + * message of the currently selected conversation, otherwise mark the + * message to be set for the next selected conversation. + * + * @param {string} message - The message to place in the entry + * @param {boolean} pending - Wait for a conversation to be selected + */ + setMessage(message, pending = false) { + try { + if (pending) + this._pendingShare = message; + else + this.stack.visible_child.setMessage(message); + } catch (e) { + debug(e); + } + } +}); + + +/** + * A Gtk.ApplicationWindow for selecting from open conversations + */ +var ConversationChooser = GObject.registerClass({ + GTypeName: 'GSConnectConversationChooser', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'message': GObject.ParamSpec.string( + 'message', + 'Message', + 'The message to share', + GObject.ParamFlags.READWRITE, + '' + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin providing messages', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, +}, class ConversationChooser extends Gtk.ApplicationWindow { + + _init(params) { + super._init(Object.assign({ + title: _('Share Link'), + default_width: 300, + default_height: 200, + }, params)); + this.set_keep_above(true); + + // HeaderBar + this.headerbar = new Gtk.HeaderBar({ + title: _('Share Link'), + subtitle: this.message, + show_close_button: true, + tooltip_text: this.message, + }); + this.set_titlebar(this.headerbar); + + const newButton = new Gtk.Button({ + image: new Gtk.Image({icon_name: 'list-add-symbolic'}), + tooltip_text: _('New Conversation'), + always_show_image: true, + }); + newButton.connect('clicked', this._new.bind(this)); + this.headerbar.pack_start(newButton); + + // Threads + const scrolledWindow = new Gtk.ScrolledWindow({ + can_focus: false, + hexpand: true, + vexpand: true, + hscrollbar_policy: Gtk.PolicyType.NEVER, + }); + this.add(scrolledWindow); + + this.thread_list = new Gtk.ListBox({ + activate_on_single_click: false, + }); + this.thread_list.set_sort_func(Window.prototype._sortThreads); + this.thread_list.connect('row-activated', this._select.bind(this)); + scrolledWindow.add(this.thread_list); + + // Filter Setup + Window.prototype._onThreadsChanged.call(this); + this.show_all(); + } + + get plugin() { + return this._plugin || null; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + _new(button) { + const message = this.message; + this.destroy(); + + this.plugin.sms(); + this.plugin.window._onNewConversation(); + this.plugin.window._pendingShare = message; + } + + _select(box, row) { + this.plugin.sms(); + this.plugin.window.thread_id = row.message.thread_id.toString(); + this.plugin.window.setMessage(this.message); + + this.destroy(); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js new file mode 100644 index 0000000..3ef0fef --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/mousepad.js @@ -0,0 +1,462 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GLib = imports.gi.GLib; +const Gdk = imports.gi.Gdk; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + + +/** + * A map of Gdk to "KDE Connect" keyvals + */ +const ReverseKeyMap = new Map([ + [Gdk.KEY_BackSpace, 1], + [Gdk.KEY_Tab, 2], + [Gdk.KEY_Linefeed, 3], + [Gdk.KEY_Left, 4], + [Gdk.KEY_Up, 5], + [Gdk.KEY_Right, 6], + [Gdk.KEY_Down, 7], + [Gdk.KEY_Page_Up, 8], + [Gdk.KEY_Page_Down, 9], + [Gdk.KEY_Home, 10], + [Gdk.KEY_End, 11], + [Gdk.KEY_Return, 12], + [Gdk.KEY_Delete, 13], + [Gdk.KEY_Escape, 14], + [Gdk.KEY_Sys_Req, 15], + [Gdk.KEY_Scroll_Lock, 16], + [Gdk.KEY_F1, 21], + [Gdk.KEY_F2, 22], + [Gdk.KEY_F3, 23], + [Gdk.KEY_F4, 24], + [Gdk.KEY_F5, 25], + [Gdk.KEY_F6, 26], + [Gdk.KEY_F7, 27], + [Gdk.KEY_F8, 28], + [Gdk.KEY_F9, 29], + [Gdk.KEY_F10, 30], + [Gdk.KEY_F11, 31], + [Gdk.KEY_F12, 32], +]); + + +/* + * A list of keyvals we consider modifiers + */ +const MOD_KEYS = [ + 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, +]; + + +/* + * Some convenience functions for checking keyvals for modifiers + */ +const isAlt = (key) => [Gdk.KEY_Alt_L, Gdk.KEY_Alt_R].includes(key); +const isCtrl = (key) => [Gdk.KEY_Control_L, Gdk.KEY_Control_R].includes(key); +const isShift = (key) => [Gdk.KEY_Shift_L, Gdk.KEY_Shift_R].includes(key); +const isSuper = (key) => [Gdk.KEY_Super_L, Gdk.KEY_Super_R].includes(key); + + +var InputDialog = GObject.registerClass({ + GTypeName: 'GSConnectMousepadInputDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The mousepad plugin associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/mousepad-input-dialog.ui', + Children: [ + 'infobar', 'infobar-label', + 'touchpad-eventbox', 'mouse-left-button', 'mouse-middle-button', 'mouse-right-button', + 'touchpad-drag', 'touchpad-long-press', + 'shift-label', 'ctrl-label', 'alt-label', 'super-label', 'entry', + ], +}, class InputDialog extends Gtk.Dialog { + + _init(params) { + super._init(Object.assign({ + use_header_bar: true, + }, params)); + + const headerbar = this.get_titlebar(); + headerbar.title = _('Remote Input'); + headerbar.subtitle = this.device.name; + + // Main Box + const content = this.get_content_area(); + content.border_width = 0; + + // TRANSLATORS: Displayed when the remote keyboard is not ready to accept input + this.infobar_label.label = _('Remote keyboard on %s is not active').format(this.device.name); + + // Text Input + this.entry.buffer.connect( + 'insert-text', + this._onInsertText.bind(this) + ); + + this.infobar.connect('notify::reveal-child', this._onState.bind(this)); + this.plugin.bind_property('state', this.infobar, 'reveal-child', 6); + + // Mouse Pad + this._resetTouchpadMotion(); + this.touchpad_motion_timeout_id = 0; + this.touchpad_holding = false; + + // Scroll Input + this.add_events(Gdk.EventMask.SCROLL_MASK); + + this.show_all(); + } + + vfunc_delete_event(event) { + this._ungrab(); + return this.hide_on_delete(); + } + + vfunc_grab_broken_event(event) { + if (event.keyboard) + this._ungrab(); + + return false; + } + + vfunc_key_release_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + const keyvalLower = Gdk.keyval_to_lower(event.keyval); + const realMask = event.state & Gtk.accelerator_get_default_mod_mask(); + + this.alt_label.sensitive = !isAlt(keyvalLower) && (realMask & Gdk.ModifierType.MOD1_MASK); + this.ctrl_label.sensitive = !isCtrl(keyvalLower) && (realMask & Gdk.ModifierType.CONTROL_MASK); + this.shift_label.sensitive = !isShift(keyvalLower) && (realMask & Gdk.ModifierType.SHIFT_MASK); + this.super_label.sensitive = !isSuper(keyvalLower) && (realMask & Gdk.ModifierType.SUPER_MASK); + + return super.vfunc_key_release_event(event); + } + + vfunc_key_press_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + let keyvalLower = Gdk.keyval_to_lower(event.keyval); + let realMask = event.state & Gtk.accelerator_get_default_mod_mask(); + + this.alt_label.sensitive = isAlt(keyvalLower) || (realMask & Gdk.ModifierType.MOD1_MASK); + this.ctrl_label.sensitive = isCtrl(keyvalLower) || (realMask & Gdk.ModifierType.CONTROL_MASK); + this.shift_label.sensitive = isShift(keyvalLower) || (realMask & Gdk.ModifierType.SHIFT_MASK); + this.super_label.sensitive = isSuper(keyvalLower) || (realMask & Gdk.ModifierType.SUPER_MASK); + + // Wait for a real key before sending + if (MOD_KEYS.includes(keyvalLower)) + return false; + + // 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; + + // CapsLock isn't supported as a keybinding modifier, so keep it from + // confusing us + realMask &= ~Gdk.ModifierType.LOCK_MASK; + + if (keyvalLower === 0) + return false; + + debug(`keyval: ${event.keyval}, mask: ${realMask}`); + + const request = { + alt: !!(realMask & Gdk.ModifierType.MOD1_MASK), + ctrl: !!(realMask & Gdk.ModifierType.CONTROL_MASK), + shift: !!(realMask & Gdk.ModifierType.SHIFT_MASK), + super: !!(realMask & Gdk.ModifierType.SUPER_MASK), + sendAck: true, + }; + + // specialKey + if (ReverseKeyMap.has(event.keyval)) { + request.specialKey = ReverseKeyMap.get(event.keyval); + + // key + } else { + const codePoint = Gdk.keyval_to_unicode(event.keyval); + request.key = String.fromCodePoint(codePoint); + } + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: request, + }); + + // Pass these key combinations rather than using the echo reply + if (request.alt || request.ctrl || request.super) + return super.vfunc_key_press_event(event); + + return false; + } + + vfunc_scroll_event(event) { + if (event.delta_x === 0 && event.delta_y === 0) + return true; + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + scroll: true, + dx: event.delta_x * 200, + dy: event.delta_y * 200, + }, + }); + return true; + } + + vfunc_window_state_event(event) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + if (event.new_window_state & Gdk.WindowState.FOCUSED) + this._grab(); + else + this._ungrab(); + + return super.vfunc_window_state_event(event); + } + + _onInsertText(buffer, location, text, len) { + if (this._isAck) + return; + + debug(`insert-text: ${text} (chars ${[...text].length})`); + + for (const char of [...text]) { + if (!char) + continue; + + // TODO: modifiers? + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + alt: false, + ctrl: false, + shift: false, + super: false, + sendAck: false, + key: char, + }, + }); + } + } + + _onState(widget) { + if (!this.plugin.state) + debug('ignoring remote keyboard state'); + + if (this.is_active) + this._grab(); + else + this._ungrab(); + } + + _grab() { + if (!this.visible || this._keyboard) + return; + + const seat = Gdk.Display.get_default().get_default_seat(); + const status = seat.grab( + this.get_window(), + Gdk.SeatCapabilities.KEYBOARD, + false, + null, + null, + null + ); + + if (status !== Gdk.GrabStatus.SUCCESS) { + logError(new Error('Grabbing keyboard failed')); + return; + } + + this._keyboard = seat.get_keyboard(); + this.grab_add(); + this.entry.has_focus = true; + } + + _ungrab() { + if (this._keyboard) { + this._keyboard.get_seat().ungrab(); + this._keyboard = null; + this.grab_remove(); + } + + this.entry.buffer.text = ''; + } + + _resetTouchpadMotion() { + this.touchpad_motion_prev_x = 0; + this.touchpad_motion_prev_y = 0; + this.touchpad_motion_x = 0; + this.touchpad_motion_y = 0; + } + + _onMouseLeftButtonClicked(button) { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + singleclick: true, + }, + }); + } + + _onMouseMiddleButtonClicked(button) { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + middleclick: true, + }, + }); + } + + _onMouseRightButtonClicked(button) { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + rightclick: true, + }, + }); + } + + _onTouchpadDragBegin(gesture) { + this._resetTouchpadMotion(); + + this.touchpad_motion_timeout_id = + GLib.timeout_add(GLib.PRIORITY_DEFAULT, 10, + this._onTouchpadMotionTimeout.bind(this)); + } + + _onTouchpadDragUpdate(gesture, offset_x, offset_y) { + this.touchpad_motion_x = offset_x; + this.touchpad_motion_y = offset_y; + } + + _onTouchpadDragEnd(gesture) { + this._resetTouchpadMotion(); + + GLib.Source.remove(this.touchpad_motion_timeout_id); + this.touchpad_motion_timeout_id = 0; + } + + _onTouchpadLongPressCancelled(gesture) { + const gesture_button = gesture.get_current_button(); + + // Check user dragged less than certain distances. + const is_click = + (Math.abs(this.touchpad_motion_x) < 4) && + (Math.abs(this.touchpad_motion_y) < 4); + + if (is_click) { + var click_body = {}; + switch (gesture_button) { + case 1: + click_body.singleclick = true; + break; + + case 2: + click_body.middleclick = true; + break; + + case 3: + click_body.rightclick = true; + break; + + default: + return; + } + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: click_body, + }); + } + } + + _onTouchpadLongPressPressed(gesture) { + const gesture_button = gesture.get_current_button(); + + if (gesture_button !== 1) { + debug('Long press on other type of buttons are not handled.'); + } else { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + singlehold: true, + }, + }); + this.touchpad_holding = true; + } + } + + _onTouchpadLongPressEnd(gesture) { + if (this.touchpad_holding) { + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + singlerelease: true, + }, + }); + this.touchpad_holding = false; + } + } + + _onTouchpadMotionTimeout() { + var diff_x = this.touchpad_motion_x - this.touchpad_motion_prev_x; + var diff_y = this.touchpad_motion_y - this.touchpad_motion_prev_y; + + this.device.sendPacket({ + type: 'kdeconnect.mousepad.request', + body: { + dx: diff_x, + dy: diff_y, + }, + }); + + this.touchpad_motion_prev_x = this.touchpad_motion_x; + this.touchpad_motion_prev_y = this.touchpad_motion_y; + return true; + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js new file mode 100644 index 0000000..8cffda1 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/notification.js @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const URI = imports.service.utils.uri; +const _ui = imports.service.utils.ui; + + +/** + * A dialog for repliable notifications. + */ +var ReplyDialog = GObject.registerClass({ + GTypeName: 'GSConnectNotificationReplyDialog', + Properties: { + 'device': GObject.ParamSpec.object( + 'device', + 'Device', + 'The device associated with this window', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'plugin': GObject.ParamSpec.object( + 'plugin', + 'Plugin', + 'The plugin that owns this notification', + GObject.ParamFlags.READWRITE, + GObject.Object + ), + 'uuid': GObject.ParamSpec.string( + 'uuid', + 'UUID', + 'The notification reply UUID', + GObject.ParamFlags.READWRITE, + null + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/notification-reply-dialog.ui', + Children: ['infobar', 'notification-title', 'notification-body', 'entry'], +}, class ReplyDialog extends Gtk.Dialog { + + _init(params) { + super._init({ + application: Gio.Application.get_default(), + device: params.device, + plugin: params.plugin, + uuid: params.uuid, + use_header_bar: true, + }); + + this.set_response_sensitive(Gtk.ResponseType.OK, false); + + // Info bar + this.device.bind_property( + 'connected', + this.infobar, + 'reveal-child', + GObject.BindingFlags.INVERT_BOOLEAN + ); + + // Notification Data + const headerbar = this.get_titlebar(); + headerbar.title = params.notification.appName; + headerbar.subtitle = this.device.name; + + this.notification_title.label = params.notification.title; + this.notification_body.label = URI.linkify(params.notification.text); + + // Message Entry/Send Button + this.device.bind_property( + 'connected', + this.entry, + 'sensitive', + GObject.BindingFlags.DEFAULT + ); + + this._connectedId = this.device.connect( + 'notify::connected', + this._onStateChanged.bind(this) + ); + + this._entryChangedId = this.entry.buffer.connect( + 'changed', + this._onStateChanged.bind(this) + ); + + this.restoreGeometry('notification-reply-dialog'); + + this.connect('destroy', this._onDestroy); + } + + _onDestroy(dialog) { + dialog.entry.buffer.disconnect(dialog._entryChangedId); + dialog.device.disconnect(dialog._connectedId); + } + + vfunc_delete_event() { + this.saveGeometry(); + + return false; + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + // Refuse to send empty or whitespace only messages + if (!this.entry.buffer.text.trim()) + return; + + this.plugin.replyNotification( + this.uuid, + this.entry.buffer.text + ); + } + + this.destroy(); + } + + get device() { + if (this._device === undefined) + this._device = null; + + return this._device; + } + + set device(device) { + this._device = device; + } + + get plugin() { + if (this._plugin === undefined) + this._plugin = null; + + return this._plugin; + } + + set plugin(plugin) { + this._plugin = plugin; + } + + get uuid() { + if (this._uuid === undefined) + this._uuid = null; + + return this._uuid; + } + + set uuid(uuid) { + this._uuid = uuid; + + // We must have a UUID + if (!uuid) { + this.destroy(); + debug('no uuid for repliable notification'); + } + } + + _onActivateLink(label, uri) { + Gtk.show_uri_on_window( + this.get_toplevel(), + uri.includes('://') ? uri : `https://${uri}`, + Gtk.get_current_event_time() + ); + + return true; + } + + _onStateChanged() { + if (this.device.connected && this.entry.buffer.text.trim()) + this.set_response_sensitive(Gtk.ResponseType.OK, true); + else + this.set_response_sensitive(Gtk.ResponseType.OK, false); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/service.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/service.js new file mode 100644 index 0000000..a3dd81a --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/ui/service.js @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Config = imports.config; + + +/* + * Issue Header + */ +const ISSUE_HEADER = ` +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')} +`; + + +/** + * A dialog for selecting a device + */ +var DeviceChooser = GObject.registerClass({ + GTypeName: 'GSConnectServiceDeviceChooser', + Properties: { + 'action-name': GObject.ParamSpec.string( + 'action-name', + 'Action Name', + 'The name of the associated action, like "sendFile"', + GObject.ParamFlags.READWRITE, + null + ), + 'action-target': GObject.param_spec_variant( + 'action-target', + 'Action Target', + 'The parameter for action invocations', + new GLib.VariantType('*'), + null, + GObject.ParamFlags.READWRITE + ), + }, + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-device-chooser.ui', + Children: ['device-list', 'cancel-button', 'select-button'], +}, class DeviceChooser extends Gtk.Dialog { + + _init(params = {}) { + super._init({ + use_header_bar: true, + application: Gio.Application.get_default(), + }); + this.set_keep_above(true); + + // HeaderBar + this.get_header_bar().subtitle = params.title; + + // Dialog Action + this.action_name = params.action_name; + this.action_target = params.action_target; + + // Device List + this.device_list.set_sort_func(this._sortDevices); + + this._devicesChangedId = this.application.settings.connect( + 'changed::devices', + this._onDevicesChanged.bind(this) + ); + this._onDevicesChanged(); + } + + vfunc_response(response_id) { + if (response_id === Gtk.ResponseType.OK) { + try { + const device = this.device_list.get_selected_row().device; + device.activate_action(this.action_name, this.action_target); + } catch (e) { + logError(e); + } + } + + this.destroy(); + } + + get action_name() { + if (this._action_name === undefined) + this._action_name = null; + + return this._action_name; + } + + set action_name(name) { + this._action_name = name; + } + + get action_target() { + if (this._action_target === undefined) + this._action_target = null; + + return this._action_target; + } + + set action_target(variant) { + this._action_target = variant; + } + + _onDeviceActivated(box, row) { + this.response(Gtk.ResponseType.OK); + } + + _onDeviceSelected(box) { + this.set_response_sensitive( + Gtk.ResponseType.OK, + (box.get_selected_row()) + ); + } + + _onDevicesChanged() { + // Collect known devices + const devices = {}; + + for (const [id, device] of this.application.manager.devices.entries()) + devices[id] = device; + + // Prune device rows + this.device_list.foreach(row => { + if (!devices.hasOwnProperty(row.name)) + row.destroy(); + else + delete devices[row.name]; + }); + + // Add new devices + for (const device of Object.values(devices)) { + const action = device.lookup_action(this.action_name); + + if (action === null) + continue; + + const row = new Gtk.ListBoxRow({ + visible: action.enabled, + }); + row.set_name(device.id); + row.device = device; + + action.bind_property( + 'enabled', + row, + 'visible', + Gio.SettingsBindFlags.DEFAULT + ); + + const grid = new Gtk.Grid({ + column_spacing: 12, + margin: 6, + visible: true, + }); + row.add(grid); + + const icon = new Gtk.Image({ + icon_name: device.icon_name, + pixel_size: 32, + visible: true, + }); + grid.attach(icon, 0, 0, 1, 1); + + const name = new Gtk.Label({ + label: device.name, + halign: Gtk.Align.START, + hexpand: true, + visible: true, + }); + grid.attach(name, 1, 0, 1, 1); + + this.device_list.add(row); + } + + if (this.device_list.get_selected_row() === null) + this.device_list.select_row(this.device_list.get_row_at_index(0)); + } + + _sortDevices(row1, row2) { + return row1.device.name.localeCompare(row2.device.name); + } +}); + + +/** + * A dialog for reporting an error. + */ +var ErrorDialog = GObject.registerClass({ + GTypeName: 'GSConnectServiceErrorDialog', + Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/service-error-dialog.ui', + Children: [ + 'error-stack', + 'expander-arrow', + 'gesture', + 'report-button', + 'revealer', + ], +}, class ErrorDialog extends Gtk.Window { + + _init(error) { + super._init({ + application: Gio.Application.get_default(), + title: `GSConnect: ${error.name}`, + }); + this.set_keep_above(true); + + this.error = error; + this.error_stack.buffer.text = `${error.message}\n\n${error.stack}`; + this.gesture.connect('released', this._onReleased.bind(this)); + } + + _onClicked(button) { + if (this.report_button === button) { + const uri = this._buildUri(this.error.message, this.error.stack); + Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null); + } + + this.destroy(); + } + + _onReleased(gesture, n_press) { + if (n_press === 1) + this.revealer.reveal_child = !this.revealer.reveal_child; + } + + _onRevealChild(revealer, pspec) { + this.expander_arrow.icon_name = this.revealer.reveal_child + ? 'pan-down-symbolic' + : 'pan-end-symbolic'; + } + + _buildUri(message, stack) { + const body = `\`\`\`${ISSUE_HEADER}\n${stack}\n\`\`\``; + const titleQuery = encodeURIComponent(message).replace('%20', '+'); + const bodyQuery = encodeURIComponent(body).replace('%20', '+'); + const uri = `${Config.PACKAGE_BUGREPORT}?title=${titleQuery}&body=${bodyQuery}`; + + // Reasonable URI length limit + if (uri.length > 2000) + return uri.substr(0, 2000); + + return uri; + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js new file mode 100644 index 0000000..954f7ce --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/dbus.js @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GjsPrivate = imports.gi.GjsPrivate; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + + +/* + * Some utility methods + */ +function toDBusCase(string) { + return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => { + return ltr.toUpperCase(); + }).replace(/[\s_-]+/g, ''); +} + +function toHyphenCase(string) { + return string.replace(/(?:[A-Z])/g, (ltr, offset) => { + return (offset > 0) ? `-${ltr.toLowerCase()}` : ltr.toLowerCase(); + }).replace(/[\s_]+/g, ''); +} + +function toUnderscoreCase(string) { + return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => { + if (ltr === '_') + return ''; + + return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase(); + }).replace(/[\s-]+/g, ''); +} + + +/** + * DBus.Interface represents a DBus interface bound to an object instance, meant + * to be exported over DBus. + */ +var Interface = GObject.registerClass({ + GTypeName: 'GSConnectDBusInterface', + Implements: [Gio.DBusInterface], + Properties: { + 'g-instance': GObject.ParamSpec.object( + 'g-instance', + 'Instance', + 'The delegate GObject', + GObject.ParamFlags.READWRITE, + GObject.Object.$gtype + ), + }, +}, class Interface extends GjsPrivate.DBusImplementation { + + _init(params) { + super._init({ + g_instance: params.g_instance, + g_interface_info: params.g_interface_info, + }); + + // Cache member lookups + this._instanceHandlers = []; + this._instanceMethods = {}; + this._instanceProperties = {}; + + const info = this.get_info(); + this.connect('handle-method-call', this._call.bind(this._instance, info)); + this.connect('handle-property-get', this._get.bind(this._instance, info)); + this.connect('handle-property-set', this._set.bind(this._instance, info)); + + // Automatically forward known signals + const id = this._instance.connect('notify', this._notify.bind(this)); + this._instanceHandlers.push(id); + + for (const signal of info.signals) { + const type = `(${signal.args.map(arg => arg.signature).join('')})`; + const id = this._instance.connect( + signal.name, + this._emit.bind(this, signal.name, type) + ); + + this._instanceHandlers.push(id); + } + + // Export if connection and object path were given + if (params.g_connection && params.g_object_path) + this.export(params.g_connection, params.g_object_path); + } + + get g_instance() { + if (this._instance === undefined) + this._instance = null; + + return this._instance; + } + + set g_instance(instance) { + this._instance = instance; + } + + /** + * Invoke an instance's method for a DBus method call. + * + * @param {Gio.DBusInterfaceInfo} info - The DBus interface + * @param {DBus.Interface} iface - The DBus interface + * @param {string} name - The DBus method name + * @param {GLib.Variant} parameters - The method parameters + * @param {Gio.DBusMethodInvocation} invocation - The method invocation info + */ + async _call(info, iface, name, parameters, invocation) { + let retval; + + // Invoke the instance method + try { + const args = parameters.unpack().map(parameter => { + if (parameter.get_type_string() === 'h') { + const message = invocation.get_message(); + const fds = message.get_unix_fd_list(); + const idx = parameter.deepUnpack(); + return fds.get(idx); + } else { + return parameter.recursiveUnpack(); + } + }); + + retval = await this[name](...args); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + // likely to be a normal JS error + if (!e.name.includes('.')) + e.name = `org.gnome.gjs.JSError.${e.name}`; + + invocation.return_dbus_error(e.name, e.message); + } + + logError(e, `${this}: ${name}`); + return; + } + + // `undefined` is an empty tuple on DBus + if (retval === undefined) + retval = new GLib.Variant('()', []); + + // Return the instance result or error + try { + if (!(retval instanceof GLib.Variant)) { + const args = info.lookup_method(name).out_args; + retval = new GLib.Variant( + `(${args.map(arg => arg.signature).join('')})`, + (args.length === 1) ? [retval] : retval + ); + } + + invocation.return_value(retval); + } catch (e) { + invocation.return_dbus_error( + 'org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type' + ); + + logError(e, `${this}: ${name}`); + } + } + + _nativeProp(obj, name) { + if (this._instanceProperties[name] === undefined) { + let propName = name; + + if (propName in obj) + this._instanceProperties[name] = propName; + + if (this._instanceProperties[name] === undefined) { + propName = toUnderscoreCase(name); + + if (propName in obj) + this._instanceProperties[name] = propName; + } + } + + return this._instanceProperties[name]; + } + + _emit(name, type, obj, ...args) { + this.emit_signal(name, new GLib.Variant(type, args)); + } + + _get(info, iface, name) { + const nativeValue = this[iface._nativeProp(this, name)]; + const propertyInfo = info.lookup_property(name); + + if (nativeValue === undefined || propertyInfo === null) + return null; + + return new GLib.Variant(propertyInfo.signature, nativeValue); + } + + _set(info, iface, name, value) { + const nativeValue = value.recursiveUnpack(); + + this[iface._nativeProp(this, name)] = nativeValue; + } + + _notify(obj, pspec) { + const name = toDBusCase(pspec.name); + const propertyInfo = this.get_info().lookup_property(name); + + if (propertyInfo === null) + return; + + this.emit_property_changed( + name, + new GLib.Variant( + propertyInfo.signature, + // Adjust for GJS's '-'/'_' conversion + this._instance[pspec.name.replace(/-/gi, '_')] + ) + ); + } + + destroy() { + try { + for (const id of this._instanceHandlers) + this._instance.disconnect(id); + this._instanceHandlers = []; + + this.flush(); + this.unexport(); + } catch (e) { + logError(e); + } + } +}); + +/** + * Get a new, dedicated DBus connection on @busType + * + * @param {Gio.BusType} [busType] - a Gio.BusType constant + * @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable + * @return {Promise<Gio.DBusConnection>} A new DBus connection + */ +function newConnection(busType = Gio.BusType.SESSION, cancellable = null) { + return new Promise((resolve, reject) => { + Gio.DBusConnection.new_for_address( + Gio.dbus_address_get_for_bus_sync(busType, cancellable), + Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT | + Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION, + null, + cancellable, + (connection, res) => { + try { + resolve(Gio.DBusConnection.new_for_address_finish(res)); + } catch (e) { + reject(e); + } + } + ); + + }); +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/setup.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/setup.js new file mode 100644 index 0000000..2f9f6c1 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/setup.js @@ -0,0 +1,433 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GIRepository = imports.gi.GIRepository; +const GLib = imports.gi.GLib; + +const Config = imports.config; +const {setup, setupGettext} = imports.utils.setup; + + +// Promise Wrappers +try { + const {EBook, EDataServer} = imports.gi; + + Gio._promisify(EBook.BookClient, 'connect'); + Gio._promisify(EBook.BookClient.prototype, 'get_view'); + Gio._promisify(EBook.BookClient.prototype, 'get_contacts'); + Gio._promisify(EDataServer.SourceRegistry, 'new'); +} catch (e) { + // Silence import errors +} + +Gio._promisify(Gio.AsyncInitable.prototype, 'init_async'); +Gio._promisify(Gio.DBusConnection.prototype, 'call'); +Gio._promisify(Gio.DBusProxy.prototype, 'call'); +Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async', + 'read_line_finish_utf8'); +Gio._promisify(Gio.File.prototype, 'delete_async'); +Gio._promisify(Gio.File.prototype, 'enumerate_children_async'); +Gio._promisify(Gio.File.prototype, 'load_contents_async'); +Gio._promisify(Gio.File.prototype, 'mount_enclosing_volume'); +Gio._promisify(Gio.File.prototype, 'query_info_async'); +Gio._promisify(Gio.File.prototype, 'read_async'); +Gio._promisify(Gio.File.prototype, 'replace_async'); +Gio._promisify(Gio.File.prototype, 'replace_contents_bytes_async', + 'replace_contents_finish'); +Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async'); +Gio._promisify(Gio.Mount.prototype, 'unmount_with_operation'); +Gio._promisify(Gio.InputStream.prototype, 'close_async'); +Gio._promisify(Gio.OutputStream.prototype, 'close_async'); +Gio._promisify(Gio.OutputStream.prototype, 'splice_async'); +Gio._promisify(Gio.OutputStream.prototype, 'write_all_async'); +Gio._promisify(Gio.SocketClient.prototype, 'connect_async'); +Gio._promisify(Gio.SocketListener.prototype, 'accept_async'); +Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async'); +Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async'); +Gio._promisify(Gio.TlsConnection.prototype, 'handshake_async'); +Gio._promisify(Gio.DtlsConnection.prototype, 'handshake_async'); + + +// User Directories +Config.CACHEDIR = GLib.build_filenamev([GLib.get_user_cache_dir(), 'gsconnect']); +Config.CONFIGDIR = GLib.build_filenamev([GLib.get_user_config_dir(), 'gsconnect']); +Config.RUNTIMEDIR = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gsconnect']); + +// Bootstrap +setup(Config.PACKAGE_DATADIR); +setupGettext(); + +if (Config.IS_USER) { + // Infer libdir by assuming gnome-shell shares a common prefix with gjs; + // assume the parent directory if it's not there + let libdir = GIRepository.Repository.get_search_path().find(path => { + return path.endsWith('/gjs/girepository-1.0'); + }).replace('/gjs/girepository-1.0', ''); + + const gsdir = GLib.build_filenamev([libdir, 'gnome-shell']); + + if (!GLib.file_test(gsdir, GLib.FileTest.IS_DIR)) { + const currentDir = `/${GLib.path_get_basename(libdir)}`; + libdir = libdir.replace(currentDir, ''); + } + + Config.GNOME_SHELL_LIBDIR = libdir; +} + + +// Load DBus interfaces +Config.DBUS = (() => { + const bytes = Gio.resources_lookup_data( + GLib.build_filenamev([Config.APP_PATH, `${Config.APP_ID}.xml`]), + Gio.ResourceLookupFlags.NONE + ); + + const xml = new TextDecoder().decode(bytes.toArray()); + const dbus = Gio.DBusNodeInfo.new_for_xml(xml); + dbus.nodes.forEach(info => info.cache_build()); + + return dbus; +})(); + + +// Init User Directories +for (const path of [Config.CACHEDIR, Config.CONFIGDIR, Config.RUNTIMEDIR]) + GLib.mkdir_with_parents(path, 0o755); + + +/** + * Check if we're in a Wayland session (mostly for input synthesis) + * https://wiki.gnome.org/Accessibility/Wayland#Bugs.2FIssues_We_Must_Address + */ +globalThis.HAVE_REMOTEINPUT = GLib.getenv('GDMSESSION') !== 'ubuntu-wayland'; +globalThis.HAVE_WAYLAND = GLib.getenv('XDG_SESSION_TYPE') === 'wayland'; +globalThis.HAVE_GNOME = GLib.getenv('GNOME_SETUP_DISPLAY') !== null; + + +/** + * A custom debug function that logs at LEVEL_MESSAGE to avoid the need for env + * variables to be set. + * + * @param {Error|string} message - A string or Error to log + * @param {string} [prefix] - An optional prefix for the warning + */ +const _debugCallerMatch = new RegExp(/([^@]*)@([^:]*):([^:]*)/); +// eslint-disable-next-line func-style +const _debugFunc = function (error, prefix = null) { + let caller, message; + + if (error.stack) { + caller = error.stack.split('\n')[0]; + message = `${error.message}\n${error.stack}`; + } else { + caller = (new Error()).stack.split('\n')[1]; + message = JSON.stringify(error, null, 2); + } + + if (prefix) + message = `${prefix}: ${message}`; + + const [, func, file, line] = _debugCallerMatch.exec(caller); + const script = file.replace(Config.PACKAGE_DATADIR, ''); + + GLib.log_structured('GSConnect', GLib.LogLevelFlags.LEVEL_MESSAGE, { + 'MESSAGE': `[${script}:${func}:${line}]: ${message}`, + 'SYSLOG_IDENTIFIER': 'org.gnome.Shell.Extensions.GSConnect', + 'CODE_FILE': file, + 'CODE_FUNC': func, + 'CODE_LINE': line, + }); +}; + +// Swap the function out for a no-op anonymous function for speed +const settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true), +}); + +settings.connect('changed::debug', (settings, key) => { + globalThis.debug = settings.get_boolean(key) ? _debugFunc : () => {}; +}); + +if (settings.get_boolean('debug')) + globalThis.debug = _debugFunc; +else + globalThis.debug = () => {}; + + +/** + * Start wl_clipboard if not under Gnome + */ +if (!globalThis.HAVE_GNOME) { + debug('Not running as a Gnome extension'); + imports.wl_clipboard.watchService(); +} + + +/** + * A simple (for now) pre-comparison sanitizer for phone numbers + * See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210 + * + * @return {string} Return the string stripped of leading 0, and ' ()-+' + */ +String.prototype.toPhoneNumber = function () { + const strippedNumber = this.replace(/^0*|[ ()+-]/g, ''); + + if (strippedNumber.length) + return strippedNumber; + + return this; +}; + + +/** + * A simple equality check for phone numbers based on `toPhoneNumber()` + * + * @param {string} number - A phone number string to compare + * @return {boolean} If `this` and @number are equivalent phone numbers + */ +String.prototype.equalsPhoneNumber = function (number) { + const a = this.toPhoneNumber(); + const b = number.toPhoneNumber(); + + return (a.length && b.length && (a.endsWith(b) || b.endsWith(a))); +}; + + +/** + * An implementation of `rm -rf` in Gio + * + * @param {Gio.File|string} file - a GFile or filepath + */ +Gio.File.rm_rf = function (file) { + try { + if (typeof file === 'string') + file = Gio.File.new_for_path(file); + + try { + const iter = file.enumerate_children( + 'standard::name', + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null + ); + + let info; + + while ((info = iter.next_file(null))) + Gio.File.rm_rf(iter.get_child(info)); + + iter.close(null); + } catch (e) { + // Silence errors + } + + file.delete(null); + } catch (e) { + // Silence errors + } +}; + + +/** + * Extend GLib.Variant with a static method to recursively pack a variant + * + * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal. + * @return {GLib.Variant} The resulting GVariant + */ +function _full_pack(obj) { + let packed; + const type = typeof obj; + + switch (true) { + case (obj instanceof GLib.Variant): + return obj; + + case (type === 'string'): + return GLib.Variant.new('s', obj); + + case (type === 'number'): + return GLib.Variant.new('d', obj); + + case (type === 'boolean'): + return GLib.Variant.new('b', obj); + + case (obj instanceof Uint8Array): + return GLib.Variant.new('ay', obj); + + case (obj === null): + return GLib.Variant.new('mv', null); + + case (typeof obj.map === 'function'): + return GLib.Variant.new( + 'av', + obj.filter(e => e !== undefined).map(e => _full_pack(e)) + ); + + case (obj instanceof Gio.Icon): + return obj.serialize(); + + case (type === 'object'): + packed = {}; + + for (const [key, val] of Object.entries(obj)) { + if (val !== undefined) + packed[key] = _full_pack(val); + } + + return GLib.Variant.new('a{sv}', packed); + + default: + throw Error(`Unsupported type '${type}': ${obj}`); + } +} + +GLib.Variant.full_pack = _full_pack; + + +/** + * Extend GLib.Variant with a method to recursively deepUnpack() a variant + * + * @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal. + * @return {*} The resulting object + */ +function _full_unpack(obj) { + obj = (obj === undefined) ? this : obj; + const unpacked = {}; + + switch (true) { + case (obj === null): + return obj; + + case (obj instanceof GLib.Variant): + return _full_unpack(obj.deepUnpack()); + + case (obj instanceof Uint8Array): + return obj; + + case (typeof obj.map === 'function'): + return obj.map(e => _full_unpack(e)); + + case (typeof obj === 'object'): + for (const [key, value] of Object.entries(obj)) { + // Try to detect and deserialize GIcons + try { + if (key === 'icon' && value.get_type_string() === '(sv)') + unpacked[key] = Gio.Icon.deserialize(value); + else + unpacked[key] = _full_unpack(value); + } catch (e) { + unpacked[key] = _full_unpack(value); + } + } + + return unpacked; + + default: + return obj; + } +} + +GLib.Variant.prototype.full_unpack = _full_unpack; + + +/** + * Creates a GTlsCertificate from the PEM-encoded data in @cert_path and + * @key_path. If either are missing a new pair will be generated. + * + * Additionally, the private key will be added using ssh-add to allow sftp + * connections using Gio. + * + * See: https://github.com/KDE/kdeconnect-kde/blob/master/core/kdeconnectconfig.cpp#L119 + * + * @param {string} certPath - Absolute path to a x509 certificate in PEM format + * @param {string} keyPath - Absolute path to a private key in PEM format + * @param {string} commonName - A unique common name for the certificate + * @return {Gio.TlsCertificate} A TLS certificate + */ +Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) { + // Check if the certificate/key pair already exists + const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS); + const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS); + + // Create a new certificate and private key if necessary + if (!certExists || !keyExists) { + // If we weren't passed a common name, generate a random one + if (!commonName) + commonName = GLib.uuid_string_random(); + + const proc = new Gio.Subprocess({ + argv: [ + Config.OPENSSL_PATH, 'req', + '-new', '-x509', '-sha256', + '-out', certPath, + '-newkey', 'rsa:4096', '-nodes', + '-keyout', keyPath, + '-days', '3650', + '-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`, + ], + flags: (Gio.SubprocessFlags.STDOUT_SILENCE | + Gio.SubprocessFlags.STDERR_SILENCE), + }); + proc.init(null); + proc.wait_check(null); + } + + return Gio.TlsCertificate.new_from_files(certPath, keyPath); +}; + +Object.defineProperties(Gio.TlsCertificate.prototype, { + /** + * The common name of the certificate. + */ + 'common_name': { + get: function () { + if (!this.__common_name) { + const proc = new Gio.Subprocess({ + argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-subject', '-inform', 'pem'], + flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE, + }); + proc.init(null); + + const stdout = proc.communicate_utf8(this.certificate_pem, null)[1]; + this.__common_name = /(?:cn|CN) ?= ?([^,\n]*)/.exec(stdout)[1]; + } + + return this.__common_name; + }, + configurable: true, + enumerable: true, + }, + + /** + * Get just the pubkey as a DER ByteArray of a certificate. + * + * @return {GLib.Bytes} The pubkey as DER of the certificate. + */ + 'pubkey_der': { + value: function () { + if (!this.__pubkey_der) { + let proc = new Gio.Subprocess({ + argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-pubkey', '-inform', 'pem'], + flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE, + }); + proc.init(null); + + const pubkey = proc.communicate_utf8(this.certificate_pem, null)[1]; + proc = new Gio.Subprocess({ + argv: [Config.OPENSSL_PATH, 'pkey', '-pubin', '-inform', 'pem', '-outform', 'der'], + flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE, + }); + proc.init(null); + this.__pubkey_der = proc.communicate(new TextEncoder().encode(pubkey), null)[1]; + } + + return this.__pubkey_der; + }, + configurable: true, + enumerable: false, + }, + +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/ui.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/ui.js new file mode 100644 index 0000000..35922d3 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/ui.js @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gdk = imports.gi.Gdk; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const Gtk = imports.gi.Gtk; + +const Config = imports.config; + + +/* + * Window State + */ +Gtk.Window.prototype.restoreGeometry = function (context = 'default') { + this._windowState = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect.WindowState', + true + ), + path: `/org/gnome/shell/extensions/gsconnect/${context}/`, + }); + + // 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(); +}; + +Gtk.Window.prototype.saveGeometry = function () { + 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)); +}; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js new file mode 100644 index 0000000..6104ed9 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/service/utils/uri.js @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const GLib = imports.gi.GLib; + + +/** + * The same regular expression used in GNOME Shell + * + * http://daringfireball.net/2010/07/improved_regex_for_matching_urls + */ +const _balancedParens = '\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))*\\)'; +const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]'; +const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u201C\u201D\u2018\u2019]'; + +const _urlRegexp = new RegExp( + '(^|' + _leadingJunk + ')' + + '(' + + '(?:' + + '(?:http|https)://' + // scheme:// + '|' + + 'www\\d{0,3}[.]' + // www. + '|' + + '[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/ + ')' + + '(?:' + // one or more: + '[^\\s()<>]+' + // run of non-space non-() + '|' + // or + _balancedParens + // balanced parens + ')+' + + '(?:' + // end with: + _balancedParens + // balanced parens + '|' + // or + _notTrailingJunk + // last non-junk char + ')' + + ')', 'gi'); + + +/** + * sms/tel URI RegExp (https://tools.ietf.org/html/rfc5724) + * + * A fairly lenient regexp for sms: URIs that allows tel: numbers with chars + * from global-number, local-number (without phone-context) and single spaces. + * This allows passing numbers directly from libfolks or GData without + * pre-processing. It also makes an allowance for URIs passed from Gio.File + * that always come in the form "sms:///". + */ +const _smsParam = "[\\w.!~*'()-]+=(?:[\\w.!~*'()-]|%[0-9A-F]{2})*"; +const _telParam = ";[a-zA-Z0-9-]+=(?:[\\w\\[\\]/:&+$.!~*'()-]|%[0-9A-F]{2})+"; +const _lenientDigits = '[+]?(?:[0-9A-F*#().-]| (?! )|%20(?!%20))+'; +const _lenientNumber = `${_lenientDigits}(?:${_telParam})*`; + +const _smsRegex = new RegExp( + '^' + + 'sms:' + // scheme + '(?:[/]{2,3})?' + // Gio.File returns ":///" + '(' + // one or more... + _lenientNumber + // phone numbers + '(?:,' + _lenientNumber + ')*' + // separated by commas + ')' + + '(?:\\?(' + // followed by optional... + _smsParam + // parameters... + '(?:&' + _smsParam + ')*' + // separated by "&" (unescaped) + '))?' + + '$', 'g'); // fragments (#foo) not allowed + + +const _numberRegex = new RegExp( + '^' + + '(' + _lenientDigits + ')' + // phone number digits + '((?:' + _telParam + ')*)' + // followed by optional parameters + '$', 'g'); + + +/** + * Searches @str for URLs and returns an array of objects with %url + * properties showing the matched URL string, and %pos properties indicating + * the position within @str where the URL was found. + * + * @param {string} str - the string to search + * @return {Object[]} the list of match objects, as described above + */ +function findUrls(str) { + _urlRegexp.lastIndex = 0; + + const res = []; + let match; + + while ((match = _urlRegexp.exec(str))) { + const name = match[2]; + const url = GLib.uri_parse_scheme(name) ? name : `http://${name}`; + res.push({name, url, pos: match.index + match[1].length}); + } + + return res; +} + + +/** + * Return a string with URLs couched in <a> tags, parseable by Pango and + * using the same RegExp as GNOME Shell. + * + * @param {string} str - The string to be modified + * @param {string} [title] - An optional title (eg. alt text, tooltip) + * @return {string} the modified text + */ +function linkify(str, title = null) { + const text = GLib.markup_escape_text(str, -1); + + _urlRegexp.lastIndex = 0; + + if (title) { + return text.replace( + _urlRegexp, + `$1<a href="$2" title="${title}">$2</a>` + ); + } else { + return text.replace(_urlRegexp, '$1<a href="$2">$2</a>'); + } +} + + +/** + * A simple parsing class for sms: URI's (https://tools.ietf.org/html/rfc5724) + */ +var SmsURI = class URI { + constructor(uri) { + _smsRegex.lastIndex = 0; + const [, recipients, query] = _smsRegex.exec(uri); + + this.recipients = recipients.split(',').map(recipient => { + _numberRegex.lastIndex = 0; + const [, number, params] = _numberRegex.exec(recipient); + + if (params) { + for (const param of params.substr(1).split(';')) { + const [key, value] = param.split('='); + + // add phone-context to beginning of + if (key === 'phone-context' && value.startsWith('+')) + return value + unescape(number); + } + } + + return unescape(number); + }); + + if (query) { + for (const field of query.split('&')) { + const [key, value] = field.split('='); + + if (key === 'body') { + if (this.body) + throw URIError('duplicate "body" field'); + + this.body = value ? decodeURIComponent(value) : undefined; + } + } + } + } + + toString() { + const uri = `sms:${this.recipients.join(',')}`; + + return this.body ? `${uri}?body=${escape(this.body)}` : uri; + } +}; + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js new file mode 100644 index 0000000..c9b61f3 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/clipboard.js @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; +import GjsPrivate from 'gi://GjsPrivate'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + +import Meta from 'gi://Meta'; + + +/* + * DBus Interface Info + */ +const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard'; +const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard'; +const DBUS_NODE = Gio.DBusNodeInfo.new_for_xml(` +<node> + <interface name="org.gnome.Shell.Extensions.GSConnect.Clipboard"> + <!-- Methods --> + <method name="GetMimetypes"> + <arg direction="out" type="as" name="mimetypes"/> + </method> + <method name="GetText"> + <arg direction="out" type="s" name="text"/> + </method> + <method name="SetText"> + <arg direction="in" type="s" name="text"/> + </method> + <method name="GetValue"> + <arg direction="in" type="s" name="mimetype"/> + <arg direction="out" type="ay" name="value"/> + </method> + <method name="SetValue"> + <arg direction="in" type="ay" name="value"/> + <arg direction="in" type="s" name="mimetype"/> + </method> + + <!-- Signals --> + <signal name="OwnerChange"/> + </interface> +</node> +`); +const DBUS_INFO = DBUS_NODE.lookup_interface(DBUS_NAME); + + +/* + * Text Mimetypes + */ +const TEXT_MIMETYPES = [ + 'text/plain;charset=utf-8', + 'UTF8_STRING', + 'text/plain', + 'STRING', +]; + + +/* GSConnectClipboardPortal: + * + * A simple clipboard portal, especially useful on Wayland where GtkClipboard + * doesn't work in the background. + */ +export const Clipboard = GObject.registerClass({ + GTypeName: 'GSConnectShellClipboard', +}, class GSConnectShellClipboard extends GjsPrivate.DBusImplementation { + + _init(params = {}) { + super._init({ + g_interface_info: DBUS_INFO, + }); + + this._transferring = false; + + // Watch global selection + this._selection = global.display.get_selection(); + this._ownerChangedId = this._selection.connect( + 'owner-changed', + this._onOwnerChanged.bind(this) + ); + + // Prepare DBus interface + this._handleMethodCallId = this.connect( + 'handle-method-call', + this._onHandleMethodCall.bind(this) + ); + + this._nameId = Gio.DBus.own_name( + Gio.BusType.SESSION, + DBUS_NAME, + Gio.BusNameOwnerFlags.NONE, + this._onBusAcquired.bind(this), + null, + this._onNameLost.bind(this) + ); + } + + _onOwnerChanged(selection, type, source) { + /* We're only interested in the standard clipboard */ + if (type !== Meta.SelectionType.SELECTION_CLIPBOARD) + return; + + /* In Wayland an intermediate GMemoryOutputStream is used which triggers + * a second ::owner-changed emission, so we need to ensure we ignore + * that while the transfer is resolving. + */ + if (this._transferring) + return; + + this._transferring = true; + + /* We need to put our signal emission in an idle callback to ensure that + * Mutter's internal calls have finished resolving in the loop, or else + * we'll end up with the previous selection's content. + */ + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + this.emit_signal('OwnerChange', null); + this._transferring = false; + + return GLib.SOURCE_REMOVE; + }); + } + + _onBusAcquired(connection, name) { + try { + this.export(connection, DBUS_PATH); + } catch (e) { + logError(e); + } + } + + _onNameLost(connection, name) { + try { + this.unexport(); + } catch (e) { + logError(e); + } + } + + async _onHandleMethodCall(iface, name, parameters, invocation) { + let retval; + + try { + const args = parameters.recursiveUnpack(); + + retval = await this[name](...args); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + if (!e.name.includes('.')) + e.name = `org.gnome.gjs.JSError.${e.name}`; + + invocation.return_dbus_error(e.name, e.message); + } + + return; + } + + if (retval === undefined) + retval = new GLib.Variant('()', []); + + try { + if (!(retval instanceof GLib.Variant)) { + const args = DBUS_INFO.lookup_method(name).out_args; + retval = new GLib.Variant( + `(${args.map(arg => arg.signature).join('')})`, + (args.length === 1) ? [retval] : retval + ); + } + + invocation.return_value(retval); + + // Without a response, the client will wait for timeout + } catch (e) { + invocation.return_dbus_error( + 'org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type' + ); + } + } + + /** + * Get the available mimetypes of the current clipboard content + * + * @return {Promise<string[]>} A list of mime-types + */ + GetMimetypes() { + return new Promise((resolve, reject) => { + try { + const mimetypes = this._selection.get_mimetypes( + Meta.SelectionType.SELECTION_CLIPBOARD + ); + + resolve(mimetypes); + } catch (e) { + reject(e); + } + }); + } + + /** + * Get the text content of the clipboard + * + * @return {Promise<string>} Text content of the clipboard + */ + GetText() { + return new Promise((resolve, reject) => { + const mimetypes = this._selection.get_mimetypes( + Meta.SelectionType.SELECTION_CLIPBOARD); + + const mimetype = TEXT_MIMETYPES.find(type => mimetypes.includes(type)); + + if (mimetype !== undefined) { + const stream = Gio.MemoryOutputStream.new_resizable(); + + this._selection.transfer_async( + Meta.SelectionType.SELECTION_CLIPBOARD, + mimetype, -1, + stream, null, + (selection, res) => { + try { + selection.transfer_finish(res); + + const bytes = stream.steal_as_bytes(); + const bytearray = bytes.get_data(); + + resolve(new TextDecoder().decode(bytearray)); + } catch (e) { + reject(e); + } + } + ); + } else { + reject(new Error('text not available')); + } + }); + } + + /** + * Set the text content of the clipboard + * + * @param {string} text - text content to set + * @return {Promise} A promise for the operation + */ + SetText(text) { + return new Promise((resolve, reject) => { + try { + if (typeof text !== 'string') { + throw new Gio.DBusError({ + code: Gio.DBusError.INVALID_ARGS, + message: 'expected string', + }); + } + + const source = Meta.SelectionSourceMemory.new( + 'text/plain;charset=utf-8', GLib.Bytes.new(text)); + + this._selection.set_owner( + Meta.SelectionType.SELECTION_CLIPBOARD, source); + + resolve(); + } catch (e) { + reject(e); + } + }); + } + + /** + * Get the content of the clipboard with the type @mimetype. + * + * @param {string} mimetype - the mimetype to request + * @return {Promise<Uint8Array>} The content of the clipboard + */ + GetValue(mimetype) { + return new Promise((resolve, reject) => { + const stream = Gio.MemoryOutputStream.new_resizable(); + + this._selection.transfer_async( + Meta.SelectionType.SELECTION_CLIPBOARD, + mimetype, -1, + stream, null, + (selection, res) => { + try { + selection.transfer_finish(res); + + const bytes = stream.steal_as_bytes(); + + resolve(bytes.get_data()); + } catch (e) { + reject(e); + } + } + ); + }); + } + + /** + * Set the content of the clipboard to @value with the type @mimetype. + * + * @param {Uint8Array} value - the value to set + * @param {string} mimetype - the mimetype of the value + * @return {Promise} - A promise for the operation + */ + SetValue(value, mimetype) { + return new Promise((resolve, reject) => { + try { + const source = Meta.SelectionSourceMemory.new(mimetype, + GLib.Bytes.new(value)); + + this._selection.set_owner( + Meta.SelectionType.SELECTION_CLIPBOARD, source); + + resolve(); + } catch (e) { + reject(e); + } + }); + } + + destroy() { + if (this._selection && this._ownerChangedId > 0) { + this._selection.disconnect(this._ownerChangedId); + this._ownerChangedId = 0; + } + + if (this._nameId > 0) { + Gio.bus_unown_name(this._nameId); + this._nameId = 0; + } + + if (this._handleMethodCallId > 0) { + this.disconnect(this._handleMethodCallId); + this._handleMethodCallId = 0; + this.unexport(); + } + } +}); + + +let _portal = null; +let _portalId = 0; + +/** + * Watch for the service to start and export the clipboard portal when it does. + */ +export function watchService() { + if (GLib.getenv('XDG_SESSION_TYPE') !== 'wayland') + return; + + if (_portalId > 0) + return; + + _portalId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.Shell.Extensions.GSConnect', + Gio.BusNameWatcherFlags.NONE, + () => { + if (_portal === null) + _portal = new Clipboard(); + }, + () => { + if (_portal !== null) { + _portal.destroy(); + _portal = null; + } + } + ); +} + +/** + * Stop watching the service and export the portal if currently running. + */ +export function unwatchService() { + if (_portalId > 0) { + Gio.bus_unwatch_name(_portalId); + _portalId = 0; + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/device.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/device.js new file mode 100644 index 0000000..59e4d96 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/device.js @@ -0,0 +1,380 @@ +// 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); + } +}); + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js new file mode 100644 index 0000000..62ae0f1 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/gmenu.js @@ -0,0 +1,647 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Atk from 'gi://Atk'; +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; + +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +import {getIcon} from './utils.js'; + +import Tooltip from './tooltip.js'; + + +/** + * Get a dictionary of a GMenuItem's attributes + * + * @param {Gio.MenuModel} model - The menu model containing the item + * @param {number} index - The index of the item in @model + * @return {Object} A dictionary of the item's attributes + */ +function getItemInfo(model, index) { + const info = { + target: null, + links: [], + }; + + // + let iter = model.iterate_item_attributes(index); + + while (iter.next()) { + const name = iter.get_name(); + let value = iter.get_value(); + + switch (name) { + case 'icon': + value = Gio.Icon.deserialize(value); + + if (value instanceof Gio.ThemedIcon) + value = getIcon(value.names[0]); + + info[name] = value; + break; + + case 'target': + info[name] = value; + break; + + default: + info[name] = value.unpack(); + } + } + + // Submenus & Sections + iter = model.iterate_item_links(index); + + while (iter.next()) { + info.links.push({ + name: iter.get_name(), + value: iter.get_value(), + }); + } + + return info; +} + + +/** + * + */ +export class ListBox extends PopupMenu.PopupMenuSection { + + constructor(params) { + super(); + Object.assign(this, params); + + // Main Actor + this.actor = new St.BoxLayout({ + x_expand: true, + clip_to_allocation: true, + }); + this.actor._delegate = this; + + // Item Box + this.box.clip_to_allocation = true; + this.box.x_expand = true; + this.box.add_style_class_name('gsconnect-list-box'); + this.box.set_pivot_point(1, 1); + this.actor.add_child(this.box); + + // Submenu Container + this.sub = new St.BoxLayout({ + clip_to_allocation: true, + vertical: false, + visible: false, + x_expand: true, + }); + this.sub.set_pivot_point(1, 1); + this.sub._delegate = this; + this.actor.add_child(this.sub); + + // Handle transitions + this._boxTransitionsCompletedId = this.box.connect( + 'transitions-completed', + this._onTransitionsCompleted.bind(this) + ); + + this._subTransitionsCompletedId = this.sub.connect( + 'transitions-completed', + this._onTransitionsCompleted.bind(this) + ); + + // Handle keyboard navigation + this._submenuCloseKeyId = this.sub.connect( + 'key-press-event', + this._onSubmenuCloseKey.bind(this) + ); + + // Refresh the menu when mapped + this._mappedId = this.actor.connect( + 'notify::mapped', + this._onMapped.bind(this) + ); + + // Watch the model for changes + this._itemsChangedId = this.model.connect( + 'items-changed', + this._onItemsChanged.bind(this) + ); + this._onItemsChanged(); + } + + _onMapped(actor) { + if (actor.mapped) { + this._onItemsChanged(); + + // We use this instead of close() to avoid touching finalized objects + } else { + this.box.set_opacity(255); + this.box.set_width(-1); + this.box.set_height(-1); + this.box.visible = true; + + this._submenu = null; + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + this.sub.visible = false; + this.sub.get_children().map(menu => menu.hide()); + } + } + + _onSubmenuCloseKey(actor, event) { + if (this.submenu && event.get_key_symbol() === Clutter.KEY_Left) { + this.submenu.submenu_for.setActive(true); + this.submenu = null; + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _onSubmenuOpenKey(actor, event) { + const item = actor._delegate; + + if (item.submenu && event.get_key_symbol() === Clutter.KEY_Right) { + this.submenu = item.submenu; + item.submenu.firstMenuItem.setActive(true); + } + + return Clutter.EVENT_PROPAGATE; + } + + _onGMenuItemActivate(item, event) { + this.emit('activate', item); + + if (item.submenu) { + this.submenu = item.submenu; + } else if (item.action_name) { + this.action_group.activate_action( + item.action_name, + item.action_target + ); + this.itemActivated(); + } + } + + _addGMenuItem(info) { + const item = new PopupMenu.PopupMenuItem(info.label); + this.addMenuItem(item); + + if (info.action !== undefined) { + item.action_name = info.action.split('.')[1]; + item.action_target = info.target; + + item.actor.visible = this.action_group.get_action_enabled( + item.action_name + ); + } + + item.connectObject( + 'activate', + this._onGMenuItemActivate.bind(this), + this + ); + + return item; + } + + _addGMenuSection(model) { + const section = new ListBox({ + model: model, + action_group: this.action_group, + }); + this.addMenuItem(section); + } + + _addGMenuSubmenu(model, item) { + // Add an expander arrow to the item + const arrow = PopupMenu.arrowIcon(St.Side.RIGHT); + arrow.x_align = Clutter.ActorAlign.END; + arrow.x_expand = true; + item.actor.add_child(arrow); + + // Mark it as an expandable and open on right-arrow + item.actor.add_accessible_state(Atk.StateType.EXPANDABLE); + + item.actor.connect( + 'key-press-event', + this._onSubmenuOpenKey.bind(this) + ); + + // Create the submenu + item.submenu = new ListBox({ + model: model, + action_group: this.action_group, + submenu_for: item, + _parent: this, + }); + item.submenu.actor.hide(); + + // Add to the submenu container + this.sub.add_child(item.submenu.actor); + } + + _onItemsChanged(model, position, removed, added) { + // Clear the menu + this.removeAll(); + this.sub.get_children().map(child => child.destroy()); + + for (let i = 0, len = this.model.get_n_items(); i < len; i++) { + const info = getItemInfo(this.model, i); + let item; + + // A regular item + if (info.hasOwnProperty('label')) + item = this._addGMenuItem(info); + + for (const link of info.links) { + // Submenu + if (link.name === 'submenu') { + this._addGMenuSubmenu(link.value, item); + + // Section + } else if (link.name === 'section') { + this._addGMenuSection(link.value); + + // len is length starting at 1 + if (i + 1 < len) + this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + } + } + } + + // If this is a submenu of another item... + if (this.submenu_for) { + // Prepend an "<= Go Back" item, bold with a unicode arrow + const prev = new PopupMenu.PopupMenuItem(this.submenu_for.label.text); + prev.label.style = 'font-weight: bold;'; + const prevArrow = PopupMenu.arrowIcon(St.Side.LEFT); + prev.replace_child(prev._ornamentIcon, prevArrow); + this.addMenuItem(prev, 0); + + prev.connectObject('activate', (item, event) => { + this.emit('activate', item); + this._parent.submenu = null; + }, this); + } + } + + _onTransitionsCompleted(actor) { + if (this.submenu) { + this.box.visible = false; + } else { + this.sub.visible = false; + this.sub.get_children().map(menu => menu.hide()); + } + } + + get submenu() { + return this._submenu || null; + } + + set submenu(submenu) { + // Get the current allocation to hold the menu width + const allocation = this.actor.allocation; + const width = Math.max(0, allocation.x2 - allocation.x1); + + // Prepare the appropriate child for tweening + if (submenu) { + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + this.sub.visible = true; + } else { + this.box.set_opacity(0); + this.box.set_width(0); + this.sub.set_height(0); + this.box.visible = true; + } + + // Setup the animation + this.box.save_easing_state(); + this.box.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + this.box.set_easing_duration(250); + + this.sub.save_easing_state(); + this.sub.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + this.sub.set_easing_duration(250); + + if (submenu) { + submenu.actor.show(); + + this.sub.set_opacity(255); + this.sub.set_width(width); + this.sub.set_height(-1); + + this.box.set_opacity(0); + this.box.set_width(0); + this.box.set_height(0); + } else { + this.box.set_opacity(255); + this.box.set_width(width); + this.box.set_height(-1); + + this.sub.set_opacity(0); + this.sub.set_width(0); + this.sub.set_height(0); + } + + // Reset the animation + this.box.restore_easing_state(); + this.sub.restore_easing_state(); + + // + this._submenu = submenu; + } + + destroy() { + this.actor.disconnect(this._mappedId); + this.box.disconnect(this._boxTransitionsCompletedId); + this.sub.disconnect(this._subTransitionsCompletedId); + this.sub.disconnect(this._submenuCloseKeyId); + this.model.disconnect(this._itemsChangedId); + + super.destroy(); + } +} + + +/** + * A St.Button subclass for iconic GMenu items + */ +export const IconButton = GObject.registerClass({ + GTypeName: 'GSConnectShellIconButton', +}, class Button extends St.Button { + + _init(params) { + super._init({ + style_class: 'gsconnect-icon-button', + can_focus: true, + }); + Object.assign(this, params); + + // Item attributes + if (params.info.hasOwnProperty('action')) + this.action_name = params.info.action.split('.')[1]; + + if (params.info.hasOwnProperty('target')) + this.action_target = params.info.target; + + if (params.info.hasOwnProperty('label')) { + this.tooltip = new Tooltip({ + parent: this, + markup: params.info.label, + }); + + this.accessible_name = params.info.label; + } + + if (params.info.hasOwnProperty('icon')) + this.child = new St.Icon({gicon: params.info.icon}); + + // Submenu + for (const link of params.info.links) { + if (link.name === 'submenu') { + this.add_accessible_state(Atk.StateType.EXPANDABLE); + this.toggle_mode = true; + this.connect('notify::checked', this._onChecked); + + this.submenu = new ListBox({ + model: link.value, + action_group: this.action_group, + _parent: this._parent, + }); + + this.submenu.actor.style_class = 'popup-sub-menu'; + this.submenu.actor.visible = false; + } + } + } + + // This is (reliably?) emitted before ::clicked + _onChecked(button) { + if (button.checked) { + button.add_accessible_state(Atk.StateType.EXPANDED); + button.add_style_pseudo_class('active'); + } else { + button.remove_accessible_state(Atk.StateType.EXPANDED); + button.remove_style_pseudo_class('active'); + } + } + + // This is (reliably?) emitted after notify::checked + vfunc_clicked(clicked_button) { + // Unless this has a submenu, activate the action and close the menu + if (!this.toggle_mode) { + this._parent._getTopMenu().close(); + + this.action_group.activate_action( + this.action_name, + this.action_target + ); + + // StButton.checked has already been toggled so we're opening + } else if (this.checked) { + this._parent.submenu = this.submenu; + + // If this is the active submenu being closed, animate-close it + } else if (this._parent.submenu === this.submenu) { + this._parent.submenu = null; + } + } +}); + + +export class IconBox extends PopupMenu.PopupMenuSection { + + constructor(params) { + super(); + Object.assign(this, params); + + // Main Actor + this.actor = new St.BoxLayout({ + vertical: true, + x_expand: true, + }); + this.actor._delegate = this; + + // Button Box + this.box._delegate = this; + this.box.style_class = 'gsconnect-icon-box'; + this.box.vertical = false; + this.actor.add_child(this.box); + + // Submenu Container + this.sub = new St.BoxLayout({ + clip_to_allocation: true, + vertical: true, + x_expand: true, + }); + this.sub.connect('transitions-completed', this._onTransitionsCompleted); + this.sub._delegate = this; + this.actor.add_child(this.sub); + + // Track menu items so we can use ::items-changed + this._menu_items = new Map(); + + // PopupMenu + this._mappedId = this.actor.connect( + 'notify::mapped', + this._onMapped.bind(this) + ); + + // GMenu + this._itemsChangedId = this.model.connect( + 'items-changed', + this._onItemsChanged.bind(this) + ); + + // GActions + this._actionAddedId = this.action_group.connect( + 'action-added', + this._onActionChanged.bind(this) + ); + this._actionEnabledChangedId = this.action_group.connect( + 'action-enabled-changed', + this._onActionChanged.bind(this) + ); + this._actionRemovedId = this.action_group.connect( + 'action-removed', + this._onActionChanged.bind(this) + ); + } + + destroy() { + this.actor.disconnect(this._mappedId); + this.model.disconnect(this._itemsChangedId); + this.action_group.disconnect(this._actionAddedId); + this.action_group.disconnect(this._actionEnabledChangedId); + this.action_group.disconnect(this._actionRemovedId); + + super.destroy(); + } + + get submenu() { + return this._submenu || null; + } + + set submenu(submenu) { + if (submenu) { + for (const button of this.box.get_children()) { + if (button.submenu && this._submenu && button.submenu !== submenu) { + button.checked = false; + button.submenu.actor.hide(); + } + } + + this.sub.set_height(0); + submenu.actor.show(); + } + + this.sub.save_easing_state(); + this.sub.set_easing_duration(250); + this.sub.set_easing_mode(Clutter.AnimationMode.EASE_IN_OUT_CUBIC); + + this.sub.set_height(submenu ? submenu.actor.get_preferred_size()[1] : 0); + this.sub.restore_easing_state(); + + this._submenu = submenu; + } + + _onMapped(actor) { + if (!actor.mapped) { + this._submenu = null; + + for (const button of this.box.get_children()) + button.checked = false; + + for (const submenu of this.sub.get_children()) + submenu.hide(); + } + } + + _onActionChanged(group, name, enabled) { + const menuItem = this._menu_items.get(name); + + if (menuItem !== undefined) + menuItem.visible = group.get_action_enabled(name); + } + + _onItemsChanged(model, position, removed, added) { + // Remove items + while (removed > 0) { + const button = this.box.get_child_at_index(position); + const action_name = button.action_name; + + if (button.submenu) + button.submenu.destroy(); + + button.destroy(); + + this._menu_items.delete(action_name); + removed--; + } + + // Add items + for (let i = 0; i < added; i++) { + const index = position + i; + + // Create an iconic button + const button = new IconButton({ + action_group: this.action_group, + info: getItemInfo(model, index), + // NOTE: Because this doesn't derive from a PopupMenu class + // it lacks some things its parent will expect from it + _parent: this, + _delegate: null, + }); + + // Set the visibility based on the enabled state + if (button.action_name !== undefined) { + button.visible = this.action_group.get_action_enabled( + button.action_name + ); + } + + // If it has a submenu, add it as a sibling + if (button.submenu) + this.sub.add_child(button.submenu.actor); + + // Track the item if it has an action + if (button.action_name !== undefined) + this._menu_items.set(button.action_name, button); + + // Insert it in the box at the defined position + this.box.insert_child_at_index(button, index); + } + } + + _onTransitionsCompleted(actor) { + const menu = actor._delegate; + + for (const button of menu.box.get_children()) { + if (button.submenu && button.submenu !== menu.submenu) { + button.checked = false; + button.submenu.actor.hide(); + } + } + + menu.sub.set_height(-1); + } + + // PopupMenu.PopupMenuBase overrides + isEmpty() { + return (this.box.get_children().length === 0); + } + + _setParent(parent) { + super._setParent(parent); + this._onItemsChanged(this.model, 0, 0, this.model.get_n_items()); + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/input.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/input.js new file mode 100644 index 0000000..86fea67 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/input.js @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; + +import Config from '../config.mjs'; + +export class LockscreenRemoteAccess { + + constructor() { + this._inhibitor = null; + this._settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect', + null + ), + path: '/org/gnome/shell/extensions/gsconnect/', + }); + } + + patchInhibitor() { + if (this._inhibitor) + return; + + if (this._settings.get_boolean('keep-alive-when-locked')) { + this._inhibitor = global.backend.get_remote_access_controller().inhibit_remote_access; + global.backend.get_remote_access_controller().inhibit_remote_access = () => {}; + } + } + + unpatchInhibitor() { + if (!this._inhibitor) + return; + global.backend.get_remote_access_controller().inhibit_remote_access = this._inhibitor; + this._inhibitor = null; + } +} diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js new file mode 100644 index 0000000..442fa0d --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/keybindings.js @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; + + +/** + * Keybindings.Manager is a simple convenience class for managing keyboard + * shortcuts in GNOME Shell. You bind a shortcut using add(), which on success + * will return a non-zero action id that can later be used with remove() to + * unbind the shortcut. + * + * Accelerators are accepted in the form returned by Gtk.accelerator_name() and + * callbacks are invoked directly, so should be complete closures. + * + * References: + * https://developer.gnome.org/gtk3/stable/gtk3-Keyboard-Accelerators.html + * https://developer.gnome.org/meta/stable/MetaDisplay.html + * https://developer.gnome.org/meta/stable/meta-MetaKeybinding.html + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/master/js/ui/windowManager.js#L1093-1112 + */ +export class Manager { + + constructor() { + this._keybindings = new Map(); + + this._acceleratorActivatedId = global.display.connect( + 'accelerator-activated', + this._onAcceleratorActivated.bind(this) + ); + } + + _onAcceleratorActivated(display, action, inputDevice, timestamp) { + try { + const binding = this._keybindings.get(action); + + if (binding !== undefined) + binding.callback(); + } catch (e) { + logError(e); + } + } + + /** + * Add a keybinding with callback + * + * @param {string} accelerator - An accelerator in the form '<Control>q' + * @param {Function} callback - A callback for the accelerator + * @return {number} A non-zero action id on success, or 0 on failure + */ + add(accelerator, callback) { + try { + const action = global.display.grab_accelerator(accelerator, 0); + + if (action === Meta.KeyBindingAction.NONE) + throw new Error(`Failed to add keybinding: '${accelerator}'`); + + const name = Meta.external_binding_name_for_action(action); + Main.wm.allowKeybinding(name, Shell.ActionMode.ALL); + this._keybindings.set(action, {name: name, callback: callback}); + + return action; + } catch (e) { + logError(e); + } + } + + /** + * Remove a keybinding + * + * @param {number} action - A non-zero action id returned by add() + */ + remove(action) { + try { + const binding = this._keybindings.get(action); + global.display.ungrab_accelerator(action); + Main.wm.allowKeybinding(binding.name, Shell.ActionMode.NONE); + this._keybindings.delete(action); + } catch (e) { + logError(new Error(`Failed to remove keybinding: ${e.message}`)); + } + } + + /** + * Remove all keybindings + */ + removeAll() { + for (const action of this._keybindings.keys()) + this.remove(action); + } + + /** + * Destroy the keybinding manager and remove all keybindings + */ + destroy() { + global.display.disconnect(this._acceleratorActivatedId); + this.removeAll(); + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/notification.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/notification.js new file mode 100644 index 0000000..573bb97 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/notification.js @@ -0,0 +1,454 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js'; +import * as NotificationDaemon from 'resource:///org/gnome/shell/ui/notificationDaemon.js'; + +import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; +import {getIcon} from './utils.js'; + +const APP_ID = 'org.gnome.Shell.Extensions.GSConnect'; +const APP_PATH = '/org/gnome/Shell/Extensions/GSConnect'; + + +// deviceId Pattern (<device-id>|<remote-id>) +const DEVICE_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)$/); + +// requestReplyId Pattern (<device-id>|<remote-id>)|<reply-id>) +const REPLY_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)\|([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/, 'i'); + + +/** + * Extracted from notificationDaemon.js, as it's no longer exported + * https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/notificationDaemon.js#L556 + * @returns {{ 'desktop-startup-id': string }} Object with ID containing current time + */ +function getPlatformData() { + const startupId = GLib.Variant.new('s', `_TIME${global.get_current_time()}`); + return {'desktop-startup-id': startupId}; +} + +// This is no longer directly exported, so we do this instead for now +const GtkNotificationDaemon = Main.notificationDaemon._gtkNotificationDaemon.constructor; + + +/** + * A slightly modified Notification Banner with an entry field + */ +const NotificationBanner = GObject.registerClass({ + GTypeName: 'GSConnectNotificationBanner', +}, class NotificationBanner extends MessageTray.NotificationBanner { + + _init(notification) { + super._init(notification); + + if (notification.requestReplyId !== undefined) + this._addReplyAction(); + } + + _addReplyAction() { + if (!this._buttonBox) { + this._buttonBox = new St.BoxLayout({ + style_class: 'notification-actions', + x_expand: true, + }); + this.setActionArea(this._buttonBox); + global.focus_manager.add_group(this._buttonBox); + } + + // Reply Button + const button = new St.Button({ + style_class: 'notification-button', + label: _('Reply'), + x_expand: true, + can_focus: true, + }); + + button.connect( + 'clicked', + this._onEntryRequested.bind(this) + ); + + this._buttonBox.add_child(button); + + // Reply Entry + this._replyEntry = new St.Entry({ + can_focus: true, + hint_text: _('Type a message'), + style_class: 'chat-response', + x_expand: true, + visible: false, + }); + + this._buttonBox.add_child(this._replyEntry); + } + + _onEntryRequested(button) { + this.focused = true; + + for (const child of this._buttonBox.get_children()) + child.visible = (child === this._replyEntry); + + // Release the notification focus with the entry focus + this._replyEntry.connect( + 'key-focus-out', + this._onEntryDismissed.bind(this) + ); + + this._replyEntry.clutter_text.connect( + 'activate', + this._onEntryActivated.bind(this) + ); + + this._replyEntry.grab_key_focus(); + } + + _onEntryDismissed(entry) { + this.focused = false; + this.emit('unfocused'); + } + + _onEntryActivated(clutter_text) { + // Refuse to send empty replies + if (clutter_text.text === '') + return; + + // Copy the text, then clear the entry + const text = clutter_text.text; + clutter_text.text = ''; + + const {deviceId, requestReplyId} = this.notification; + + const target = new GLib.Variant('(ssbv)', [ + deviceId, + 'replyNotification', + true, + new GLib.Variant('(ssa{ss})', [requestReplyId, text, {}]), + ]); + const platformData = getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // Silence errors + } + } + ); + + this.close(); + } +}); + + +/** + * A custom notification source for spawning notifications and closing device + * notifications. This source isn't actually used, but it's methods are patched + * into existing sources. + */ +const Source = GObject.registerClass({ + GTypeName: 'GSConnectNotificationSource', +}, class Source extends NotificationDaemon.GtkNotificationDaemonAppSource { + + _closeGSConnectNotification(notification, reason) { + if (reason !== MessageTray.NotificationDestroyedReason.DISMISSED) + return; + + // Avoid sending the request multiple times + if (notification._remoteClosed || notification.remoteId === undefined) + return; + + notification._remoteClosed = true; + + const target = new GLib.Variant('(ssbv)', [ + notification.deviceId, + 'closeNotification', + true, + new GLib.Variant('s', notification.remoteId), + ]); + const platformData = getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // If we fail, reset in case we can try again + notification._remoteClosed = false; + } + } + ); + } + + /* + * Override to control notification spawning + */ + addNotification(notificationId, notificationParams, showBanner) { + this._notificationPending = true; + + // Parse the id to determine if it's a repliable notification, device + // notification or a regular local notification + let idMatch, deviceId, requestReplyId, remoteId, localId; + + if ((idMatch = REPLY_REGEX.exec(notificationId))) { + [, deviceId, remoteId, requestReplyId] = idMatch; + localId = `${deviceId}|${remoteId}`; + + } else if ((idMatch = DEVICE_REGEX.exec(notificationId))) { + [, deviceId, remoteId] = idMatch; + localId = `${deviceId}|${remoteId}`; + + } else { + localId = notificationId; + } + + // Fix themed icons + if (notificationParams.icon) { + let gicon = Gio.Icon.deserialize(notificationParams.icon); + + if (gicon instanceof Gio.ThemedIcon) { + gicon = getIcon(gicon.names[0]); + notificationParams.icon = gicon.serialize(); + } + } + + let notification = this._notifications[localId]; + + // Check if this is a repeat + if (notification) { + notification.requestReplyId = requestReplyId; + + // Bail early If @notificationParams represents an exact repeat + const title = notificationParams.title.unpack(); + const body = notificationParams.body + ? notificationParams.body.unpack() + : null; + + if (notification.title === title && + notification.bannerBodyText === body) { + this._notificationPending = false; + return; + } + + notification.title = title; + notification.bannerBodyText = body; + + // Device Notification + } else if (idMatch) { + notification = this._createNotification(notificationParams); + + notification.deviceId = deviceId; + notification.remoteId = remoteId; + notification.requestReplyId = requestReplyId; + + notification.connect('destroy', (notification, reason) => { + this._closeGSConnectNotification(notification, reason); + delete this._notifications[localId]; + }); + + this._notifications[localId] = notification; + + // Service Notification + } else { + notification = this._createNotification(notificationParams); + notification.connect('destroy', (notification, reason) => { + delete this._notifications[localId]; + }); + this._notifications[localId] = notification; + } + + if (showBanner) + this.showNotification(notification); + else + this.pushNotification(notification); + + this._notificationPending = false; + } + + /* + * Override to raise the usual notification limit (3) + */ + pushNotification(notification) { + if (this.notifications.includes(notification)) + return; + + while (this.notifications.length >= 10) + this.notifications.shift().destroy(MessageTray.NotificationDestroyedReason.EXPIRED); + + notification.connect('destroy', this._onNotificationDestroy.bind(this)); + notification.connect('notify::acknowledged', this.countUpdated.bind(this)); + this.notifications.push(notification); + this.emit('notification-added', notification); + + this.countUpdated(); + } + + createBanner(notification) { + return new NotificationBanner(notification); + } +}); + + +/** + * If there is an active GtkNotificationDaemonAppSource for GSConnect when the + * extension is loaded, it has to be patched in place. + */ +export function patchGSConnectNotificationSource() { + const source = Main.notificationDaemon._gtkNotificationDaemon._sources[APP_ID]; + + if (source !== undefined) { + // Patch in the subclassed methods + source._closeGSConnectNotification = Source.prototype._closeGSConnectNotification; + source.addNotification = Source.prototype.addNotification; + source.pushNotification = Source.prototype.pushNotification; + source.createBanner = Source.prototype.createBanner; + + // Connect to existing notifications + for (const notification of Object.values(source._notifications)) { + + const _id = notification.connect('destroy', (notification, reason) => { + source._closeGSConnectNotification(notification, reason); + notification.disconnect(_id); + }); + } + } +} + + +/** + * Wrap GtkNotificationDaemon._ensureAppSource() to patch GSConnect's app source + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/master/js/ui/notificationDaemon.js#L742-755 + */ +const __ensureAppSource = GtkNotificationDaemon.prototype._ensureAppSource; + +// eslint-disable-next-line func-style +const _ensureAppSource = function (appId) { + const source = __ensureAppSource.call(this, appId); + + if (source._appId === APP_ID) { + source._closeGSConnectNotification = Source.prototype._closeGSConnectNotification; + source.addNotification = Source.prototype.addNotification; + source.pushNotification = Source.prototype.pushNotification; + source.createBanner = Source.prototype.createBanner; + } + + return source; +}; + + +export function patchGtkNotificationDaemon() { + GtkNotificationDaemon.prototype._ensureAppSource = _ensureAppSource; +} + + +export function unpatchGtkNotificationDaemon() { + GtkNotificationDaemon.prototype._ensureAppSource = __ensureAppSource; +} + +/** + * We patch other Gtk notification sources so we can notify remote devices when + * notifications have been closed locally. + */ +const _addNotification = NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification; + +export function patchGtkNotificationSources() { + // This should diverge as little as possible from the original + // eslint-disable-next-line func-style + const addNotification = function (notificationId, notificationParams, showBanner) { + this._notificationPending = true; + + if (this._notifications[notificationId]) + this._notifications[notificationId].destroy(MessageTray.NotificationDestroyedReason.REPLACED); + + const notification = this._createNotification(notificationParams); + notification.connect('destroy', (notification, reason) => { + this._withdrawGSConnectNotification(notification, reason); + delete this._notifications[notificationId]; + }); + this._notifications[notificationId] = notification; + + if (showBanner) + this.showNotification(notification); + else + this.pushNotification(notification); + + this._notificationPending = false; + }; + + // eslint-disable-next-line func-style + const _withdrawGSConnectNotification = function (id, notification, reason) { + if (reason !== MessageTray.NotificationDestroyedReason.DISMISSED) + return; + + // Avoid sending the request multiple times + if (notification._remoteWithdrawn) + return; + + notification._remoteWithdrawn = true; + + // Recreate the notification id as it would've been sent + const target = new GLib.Variant('(ssbv)', [ + '*', + 'withdrawNotification', + true, + new GLib.Variant('s', `gtk|${this._appId}|${id}`), + ]); + const platformData = getPlatformData(); + + Gio.DBus.session.call( + APP_ID, + APP_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', ['device', [target], platformData]), + null, + Gio.DBusCallFlags.NO_AUTO_START, + -1, + null, + (connection, res) => { + try { + connection.call_finish(res); + } catch (e) { + // If we fail, reset in case we can try again + notification._remoteWithdrawn = false; + } + } + ); + }; + + NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = addNotification; + NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification = _withdrawGSConnectNotification; +} + + +export function unpatchGtkNotificationSources() { + NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = _addNotification; + delete NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification; +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js new file mode 100644 index 0000000..2996ce8 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/tooltip.js @@ -0,0 +1,309 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Clutter from 'gi://Clutter'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Pango from 'gi://Pango'; +import St from 'gi://St'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + + +/** + * An StTooltip for ClutterActors + * + * Adapted from: https://github.com/RaphaelRochet/applications-overview-tooltip + * See also: https://github.com/GNOME/gtk/blob/master/gtk/gtktooltip.c + */ +export let TOOLTIP_BROWSE_ID = 0; +export let TOOLTIP_BROWSE_MODE = false; + +export default class Tooltip { + + constructor(params) { + Object.assign(this, params); + + this._bin = null; + this._hoverTimeoutId = 0; + this._showing = false; + + this._destroyId = this.parent.connect( + 'destroy', + this.destroy.bind(this) + ); + + this._hoverId = this.parent.connect( + 'notify::hover', + this._onHover.bind(this) + ); + + this._buttonPressEventId = this.parent.connect( + 'button-press-event', + this._hide.bind(this) + ); + } + + get custom() { + if (this._custom === undefined) + this._custom = null; + + return this._custom; + } + + set custom(actor) { + this._custom = actor; + this._markup = null; + this._text = null; + + if (this._showing) + this._show(); + } + + get gicon() { + if (this._gicon === undefined) + this._gicon = null; + + return this._gicon; + } + + set gicon(gicon) { + this._gicon = gicon; + + if (this._showing) + this._show(); + } + + get icon() { + return (this.gicon) ? this.gicon.name : null; + } + + set icon(icon_name) { + if (!icon_name) + this.gicon = null; + else + this.gicon = new Gio.ThemedIcon({name: icon_name}); + } + + get markup() { + if (this._markup === undefined) + this._markup = null; + + return this._markup; + } + + set markup(text) { + this._markup = text; + this._text = null; + + if (this._showing) + this._show(); + } + + get text() { + if (this._text === undefined) + this._text = null; + + return this._text; + } + + set text(text) { + this._markup = null; + this._text = text; + + if (this._showing) + this._show(); + } + + get x_offset() { + if (this._x_offset === undefined) + this._x_offset = 0; + + return this._x_offset; + } + + set x_offset(offset) { + this._x_offset = (Number.isInteger(offset)) ? offset : 0; + } + + get y_offset() { + if (this._y_offset === undefined) + this._y_offset = 0; + + return this._y_offset; + } + + set y_offset(offset) { + this._y_offset = (Number.isInteger(offset)) ? offset : 0; + } + + _show() { + if (this.text === null && this.markup === null) + return this._hide(); + + if (this._bin === null) { + this._bin = new St.Bin({ + style_class: 'osd-window gsconnect-tooltip', + opacity: 232, + }); + + if (this.custom) { + this._bin.child = this.custom; + } else { + this._bin.child = new St.BoxLayout({vertical: false}); + + if (this.gicon) { + this._bin.child.icon = new St.Icon({ + gicon: this.gicon, + y_align: St.Align.START, + }); + this._bin.child.icon.set_y_align(Clutter.ActorAlign.START); + this._bin.child.add_child(this._bin.child.icon); + } + + this.label = new St.Label({text: this.markup || this.text}); + this.label.clutter_text.line_wrap = true; + this.label.clutter_text.line_wrap_mode = Pango.WrapMode.WORD; + this.label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + this.label.clutter_text.use_markup = (this.markup); + this._bin.child.add_child(this.label); + } + + Main.layoutManager.uiGroup.add_child(this._bin); + Main.layoutManager.uiGroup.set_child_above_sibling(this._bin, null); + } else if (this.custom) { + this._bin.child = this.custom; + } else { + if (this._bin.child.icon) + this._bin.child.icon.destroy(); + + if (this.gicon) { + this._bin.child.icon = new St.Icon({gicon: this.gicon}); + this._bin.child.insert_child_at_index(this._bin.child.icon, 0); + } + + this.label.clutter_text.text = this.markup || this.text; + this.label.clutter_text.use_markup = (this.markup); + } + + // Position tooltip + let [x, y] = this.parent.get_transformed_position(); + x = (x + (this.parent.width / 2)) - Math.round(this._bin.width / 2); + + x += this.x_offset; + y += this.y_offset; + + // Show tooltip + if (this._showing) { + this._bin.ease({ + x: x, + y: y, + time: 0.15, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } else { + this._bin.set_position(x, y); + this._bin.ease({ + opacity: 232, + time: 0.15, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + + this._showing = true; + } + + // Enable browse mode + TOOLTIP_BROWSE_MODE = true; + + if (TOOLTIP_BROWSE_ID) { + GLib.source_remove(TOOLTIP_BROWSE_ID); + TOOLTIP_BROWSE_ID = 0; + } + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + } + + _hide() { + if (this._bin) { + this._bin.ease({ + opacity: 0, + time: 0.10, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + Main.layoutManager.uiGroup.remove_actor(this._bin); + + if (this.custom) + this._bin.remove_child(this.custom); + + this._bin.destroy(); + this._bin = null; + }, + }); + } + + TOOLTIP_BROWSE_ID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => { + TOOLTIP_BROWSE_MODE = false; + TOOLTIP_BROWSE_ID = 0; + return false; + }); + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + + this._showing = false; + this._hoverTimeoutId = 0; + } + + _onHover() { + if (this.parent.hover) { + if (!this._hoverTimeoutId) { + if (this._showing) { + this._show(); + } else { + this._hoverTimeoutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + (TOOLTIP_BROWSE_MODE) ? 60 : 500, + () => { + this._show(); + this._hoverTimeoutId = 0; + return false; + } + ); + } + } + } else { + this._hide(); + } + } + + destroy() { + this.parent.disconnect(this._destroyId); + this.parent.disconnect(this._hoverId); + this.parent.disconnect(this._buttonPressEventId); + + if (this.custom) + this.custom.destroy(); + + if (this._bin) { + Main.layoutManager.uiGroup.remove_actor(this._bin); + this._bin.destroy(); + } + + if (TOOLTIP_BROWSE_ID) { + GLib.source_remove(TOOLTIP_BROWSE_ID); + TOOLTIP_BROWSE_ID = 0; + } + + if (this._hoverTimeoutId) { + GLib.source_remove(this._hoverTimeoutId); + this._hoverTimeoutId = 0; + } + } +} + diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/utils.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/utils.js new file mode 100644 index 0000000..8db091f --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/shell/utils.js @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import Gtk from 'gi://Gtk'; + +import Config from '../config.mjs'; + +let St = null; // St is not available for prefs.js importing this file. +try { + St = (await import('gi://St')).default; +} catch (e) { } + + +/** + * Get a themed icon, using fallbacks from GSConnect's GResource when necessary. + * + * @param {string} name - A themed icon name + * @return {Gio.Icon} A themed icon + */ +export function getIcon(name) { + if (getIcon._resource === undefined) { + // Setup the desktop icons + const settings = St.Settings.get(); + getIcon._desktop = new Gtk.IconTheme(); + getIcon._desktop.set_theme_name(settings.gtk_icon_theme); + settings.connect('notify::gtk-icon-theme', (settings_, key_) => { + getIcon._desktop.set_theme_name(settings_.gtk_icon_theme); + }); + + // Preload our fallbacks + const iconPath = 'resource://org/gnome/Shell/Extensions/GSConnect/icons'; + const iconNames = [ + 'org.gnome.Shell.Extensions.GSConnect', + 'org.gnome.Shell.Extensions.GSConnect-symbolic', + 'computer-symbolic', + 'laptop-symbolic', + 'smartphone-symbolic', + 'tablet-symbolic', + 'tv-symbolic', + 'phonelink-ring-symbolic', + 'sms-symbolic', + ]; + + getIcon._resource = {}; + + for (const iconName of iconNames) { + getIcon._resource[iconName] = new Gio.FileIcon({ + file: Gio.File.new_for_uri(`${iconPath}/${iconName}.svg`), + }); + } + } + + // Check the desktop icon theme + if (getIcon._desktop.has_icon(name)) + return new Gio.ThemedIcon({name: name}); + + // Check our GResource + if (getIcon._resource[name] !== undefined) + return getIcon._resource[name]; + + // Fallback to hoping it's in the theme somewhere + return new Gio.ThemedIcon({name: name}); +} + + +/** + * Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where + * necessary. + * + * @param {string} relativePath - A path relative to GSConnect's resource path + * @return {string} The file contents as a string + */ +function getResource(relativePath) { + try { + const bytes = Gio.resources_lookup_data( + GLib.build_filenamev([Config.APP_PATH, relativePath]), + Gio.ResourceLookupFlags.NONE + ); + + const source = new TextDecoder().decode(bytes.toArray()); + + return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR); + } catch (e) { + logError(e, 'GSConnect'); + return null; + } +} + + +/** + * Install file contents, to an absolute directory path. + * + * @param {string} dirname - An absolute directory path + * @param {string} basename - The file name + * @param {string} contents - The file contents + * @return {boolean} A success boolean + */ +function _installFile(dirname, basename, contents) { + try { + const filename = GLib.build_filenamev([dirname, basename]); + GLib.mkdir_with_parents(dirname, 0o755); + + return GLib.file_set_contents(filename, contents); + } catch (e) { + logError(e, 'GSConnect'); + return false; + } +} + +/** + * Install file contents from a GResource, to an absolute directory path. + * + * @param {string} dirname - An absolute directory path + * @param {string} basename - The file name + * @param {string} relativePath - A path relative to GSConnect's resource path + * @return {boolean} A success boolean + */ +function _installResource(dirname, basename, relativePath) { + try { + const contents = getResource(relativePath); + + return _installFile(dirname, basename, contents); + } catch (e) { + logError(e, 'GSConnect'); + return false; + } +} + +/** + * Use Gio.File to ensure a file's executable bits are set. + * + * @param {string} filepath - An absolute path to a file + * @returns {boolean} - True if the file already was, or is now, executable + */ +function _setExecutable(filepath) { + try { + const file = Gio.File.new_for_path(filepath); + const finfo = file.query_info( + `${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`, + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null); + + if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE)) + return false; + + const mode = finfo.get_attribute_uint32( + Gio.FILE_ATTRIBUTE_UNIX_MODE); + const new_mode = (mode | 0o111); + if (mode === new_mode) + return true; + + return file.set_attribute_uint32( + Gio.FILE_ATTRIBUTE_UNIX_MODE, + new_mode, + Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, + null); + } catch (e) { + logError(e, 'GSConnect'); + return false; + } +} + +/** + * Ensure critical files in the extension directory have the + * correct permissions. + */ +export function ensurePermissions() { + if (Config.IS_USER) { + const executableFiles = [ + 'gsconnect-preferences', + 'service/daemon.js', + 'service/nativeMessagingHost.js', + ]; + for (const file of executableFiles) + _setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file])); + } +} + +/** + * Install the files necessary for the GSConnect service to run. + */ +export function installService() { + const settings = new Gio.Settings({ + settings_schema: Config.GSCHEMA.lookup( + 'org.gnome.Shell.Extensions.GSConnect', + null + ), + path: '/org/gnome/shell/extensions/gsconnect/', + }); + + const confDir = GLib.get_user_config_dir(); + const dataDir = GLib.get_user_data_dir(); + const homeDir = GLib.get_home_dir(); + + // DBus Service + const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']); + const dbusFile = `${Config.APP_ID}.service`; + + // Desktop Entry + const appDir = GLib.build_filenamev([dataDir, 'applications']); + const appFile = `${Config.APP_ID}.desktop`; + const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`; + + // Application Icon + const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']); + const iconFull = `${Config.APP_ID}.svg`; + const iconSym = `${Config.APP_ID}-symbolic.svg`; + + // File Manager Extensions + const fileManagers = [ + [`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'], + [`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'], + ]; + + // WebExtension Manifests + const manifestFile = 'org.gnome.shell.extensions.gsconnect.json'; + const google = getResource(`webextension/${manifestFile}.google.in`); + const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`); + const manifests = [ + [`${confDir}/chromium/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google], + [`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google], + [`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google], + [`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google], + [`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google], + [`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla], + [`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google], + [`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google], + ]; + + // If running as a user extension, ensure the DBus service, desktop entry, + // file manager scripts, and WebExtension manifests are installed. + if (Config.IS_USER) { + // DBus Service + if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`)) + throw Error('GSConnect: Failed to install DBus Service'); + + // Desktop Entries + _installResource(appDir, appFile, appFile); + _installResource(appDir, appPrefsFile, appPrefsFile); + + // Application Icon + _installResource(iconDir, iconFull, `icons/${iconFull}`); + _installResource(iconDir, iconSym, `icons/${iconSym}`); + + // File Manager Extensions + const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`; + + for (const [dir, name] of fileManagers) { + const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name])); + + if (!script.query_exists(null)) { + GLib.mkdir_with_parents(dir, 0o755); + script.make_symbolic_link(target, null); + } + } + + // WebExtension Manifests + if (settings.get_boolean('create-native-messaging-hosts')) { + for (const [dirname, contents] of manifests) + _installFile(dirname, manifestFile, contents); + } + + // Otherwise, if running as a system extension, ensure anything previously + // installed when running as a user extension is removed. + } else { + GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile])); + GLib.unlink(GLib.build_filenamev([appDir, appFile])); + GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile])); + GLib.unlink(GLib.build_filenamev([iconDir, iconFull])); + GLib.unlink(GLib.build_filenamev([iconDir, iconSym])); + + for (const [dir, name] of fileManagers) + GLib.unlink(GLib.build_filenamev([dir, name])); + + for (const manifest of manifests) + GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile])); + } +} diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/stylesheet.css b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/stylesheet.css new file mode 100644 index 0000000..ea455d0 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/stylesheet.css @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/* Device Menu + +PopupMenu.PopupMenuSection.gsconnect-device-section + PopupMenu.PopupMenuSection.gsconnect-device-menu + PopupMenu.PopupSeparatorMenuItem + StLabel.gsconnect-device-name + StBoxLayout.gsconnect-device-battery + PopupMenu.PopupMenuSection + StBoxLayout.gsconnect-list-box + StBoxLayout (Submenu Container) +*/ +.gsconnect-device-section { +} + +/* Title Bar */ +.gsconnect-device-name { + font-weight: bold; +} + +.gsconnect-device-menu .popup-separator-menu-item { + margin-left: 0; + margin-right: 0; +} + +/* Battery Widget */ +.gsconnect-device-battery { + spacing: 3px; +} + +.gsconnect-device-battery StLabel { + font-size: 0.75em; +} + +.gsconnect-device-battery StIcon { + icon-size: 16px; +} + +/* Signal Strength Widget */ +.gsconnect-device-signal-strength { + spacing: 3px; +} + +.gsconnect-device-signal-strength StLabel { + font-size: 0.75em; +} + +.gsconnect-device-signal-strength StIcon { + icon-size: 16px; +} + +/* List Box */ +.gsconnect-list-box { +} + + +/* Device Panel Indicator + +PanelMenu.Button.gsconnect-device-indicator + PopupMenu.PopupMenu + PopupMenu.PopupMenuSection.gsconnect-device-menu + PopupMenu.PopupSeparatorMenuItem + StLabel.gsconnect-device-name + StBoxLayout.gsconnect-device-battery + PopupMenu.PopupMenuSection + StBoxLayout.gsconnect-icon-box + StBoxLayout (Submenu Container) + */ +.gsconnect-device-indicator { + -st-icon-style: symbolic; +} + +/* Icon Box */ +.gsconnect-icon-box { + margin: 0em 2em 0.5em; + spacing: 6px; +} + +.gsconnect-icon-button { + border-radius: 1em; + padding: 0.5em; +} + +.gsconnect-icon-button:hover, .gsconnect-icon-button:focus { + background-color: rgba(255, 255, 255, 0.125); +} + +.gsconnect-icon-button StIcon { + icon-size: 1em; +} + + +/* Tooltip + +StBin.gsconnect-tooltip (inherits from .osd-window) + StBoxLayout || [ Custom ClutterActor ] + StIcon + StLabel +*/ +.gsconnect-tooltip { + border-radius: 3px; + min-width: 0; + min-height: 0; + padding: 6px; +} + +.gsconnect-tooltip > StBoxLayout { + spacing: 6px; +} + +.gsconnect-tooltip StIcon { + icon-size: 16px; +} + +.gsconnect-tooltip StLabel { + font-weight: normal; + text-align: left; +} + +.gsconnect-tooltip StLabel:rtl { + text-align: right; +} diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.js new file mode 100644 index 0000000..232dc8c --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.js @@ -0,0 +1,519 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const SERVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect'; +const SERVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect'; +const DEVICE_NAME = 'org.gnome.Shell.Extensions.GSConnect.Device'; +const DEVICE_PATH = '/org/gnome/Shell/Extensions/GSConnect/Device'; + + +const _PROPERTIES = { + 'Connected': 'connected', + 'EncryptionInfo': 'encryption-info', + 'IconName': 'icon-name', + 'Id': 'id', + 'Name': 'name', + 'Paired': 'paired', + 'Type': 'type', +}; + + +function _proxyInit(proxy, cancellable = null) { + if (proxy.__initialized !== undefined) + return Promise.resolve(); + + return new Promise((resolve, reject) => { + proxy.init_async( + GLib.PRIORITY_DEFAULT, + cancellable, + (proxy, res) => { + try { + proxy.init_finish(res); + proxy.__initialized = true; + resolve(); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); +} + + +/** + * A simple proxy wrapper for devices exported over DBus. + */ +var Device = GObject.registerClass({ + GTypeName: 'GSConnectRemoteDevice', + Implements: [Gio.DBusInterface], + Properties: { + 'connected': GObject.ParamSpec.boolean( + 'connected', + 'Connected', + 'Whether the device is connected', + GObject.ParamFlags.READABLE, + null + ), + 'encryption-info': GObject.ParamSpec.string( + 'encryption-info', + 'Encryption Info', + 'A formatted string with the local and remote fingerprints', + GObject.ParamFlags.READABLE, + null + ), + 'icon-name': GObject.ParamSpec.string( + 'icon-name', + 'Icon Name', + 'Icon name representing the device', + GObject.ParamFlags.READABLE, + null + ), + 'id': GObject.ParamSpec.string( + 'id', + 'deviceId', + 'The device hostname or other unique id', + GObject.ParamFlags.READABLE, + '' + ), + 'name': GObject.ParamSpec.string( + 'name', + 'deviceName', + 'The device name', + GObject.ParamFlags.READABLE, + null + ), + 'paired': GObject.ParamSpec.boolean( + 'paired', + 'Paired', + 'Whether the device is paired', + GObject.ParamFlags.READABLE, + null + ), + 'type': GObject.ParamSpec.string( + 'type', + 'deviceType', + 'The device type', + GObject.ParamFlags.READABLE, + null + ), + }, +}, class Device extends Gio.DBusProxy { + + _init(service, object_path) { + this._service = service; + + super._init({ + g_connection: service.g_connection, + g_name: SERVICE_NAME, + g_object_path: object_path, + g_interface_name: DEVICE_NAME, + }); + } + + vfunc_g_properties_changed(changed, invalidated) { + try { + for (const name in changed.deepUnpack()) + this.notify(_PROPERTIES[name]); + } catch (e) { + logError(e); + } + } + + _get(name, fallback = null) { + try { + return this.get_cached_property(name).unpack(); + } catch (e) { + return fallback; + } + } + + get connected() { + return this._get('Connected', false); + } + + get encryption_info() { + return this._get('EncryptionInfo', ''); + } + + get icon_name() { + return this._get('IconName', 'computer'); + } + + get id() { + return this._get('Id', '0'); + } + + get name() { + return this._get('Name', 'Unknown'); + } + + get paired() { + return this._get('Paired', false); + } + + get service() { + return this._service; + } + + get type() { + return this._get('Type', 'desktop'); + } + + async start() { + try { + await _proxyInit(this); + + // For GActions & GMenu we pass the service's name owner to avoid + // any mixup with instances. + this.action_group = Gio.DBusActionGroup.get( + this.g_connection, + this.service.g_name_owner, + this.g_object_path + ); + + this.menu = Gio.DBusMenuModel.get( + this.g_connection, + this.service.g_name_owner, + this.g_object_path + ); + + // Poke the GMenu to ensure it's ready for us + await new Promise((resolve, reject) => { + this.g_connection.call( + SERVICE_NAME, + this.g_object_path, + 'org.gtk.Menus', + 'Start', + new GLib.Variant('(au)', [[0]]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + resolve(proxy.call_finish(res)); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } catch (e) { + this.destroy(); + throw e; + } + } + + destroy() { + GObject.signal_handlers_destroy(this); + } +}); + + +/** + * A simple proxy wrapper for the GSConnect service. + */ +var Service = GObject.registerClass({ + GTypeName: 'GSConnectRemoteService', + Implements: [Gio.DBusInterface], + Properties: { + 'active': GObject.ParamSpec.boolean( + 'active', + 'Active', + 'Whether the service is active', + GObject.ParamFlags.READABLE, + false + ), + }, + Signals: { + 'device-added': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [Device.$gtype], + }, + 'device-removed': { + flags: GObject.SignalFlags.RUN_FIRST, + param_types: [Device.$gtype], + }, + }, +}, class Service extends Gio.DBusProxy { + + _init() { + super._init({ + g_bus_type: Gio.BusType.SESSION, + g_name: SERVICE_NAME, + g_object_path: SERVICE_PATH, + g_interface_name: 'org.freedesktop.DBus.ObjectManager', + g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, + }); + + this._active = false; + this._devices = new Map(); + this._starting = false; + + // Watch the service + this._nameOwnerChangedId = this.connect( + 'notify::g-name-owner', + this._onNameOwnerChanged.bind(this) + ); + } + + get active() { + return this._active; + } + + get devices() { + return Array.from(this._devices.values()); + } + + vfunc_g_signal(sender_name, signal_name, parameters) { + try { + // Don't emit signals until the ObjectManager has started + if (!this.active) + return; + + parameters = parameters.deepUnpack(); + + switch (true) { + case (signal_name === 'InterfacesAdded'): + this._onInterfacesAdded(...parameters); + break; + + case (signal_name === 'InterfacesRemoved'): + this._onInterfacesRemoved(...parameters); + break; + } + } catch (e) { + logError(e); + } + } + + /** + * org.freedesktop.DBus.ObjectManager.InterfacesAdded + * + * @param {string} object_path - Path interfaces have been added to + * @param {Object} interfaces - A dictionary of interface objects + */ + async _onInterfacesAdded(object_path, interfaces) { + try { + // An empty list means only the object has been added + if (Object.values(interfaces).length === 0) + return; + + // Skip existing proxies + if (this._devices.has(object_path)) + return; + + // Create a proxy + const device = new Device(this, object_path); + await device.start(); + + // Hold the proxy and emit ::device-added + this._devices.set(object_path, device); + this.emit('device-added', device); + } catch (e) { + logError(e, object_path); + } + } + + /** + * org.freedesktop.DBus.ObjectManager.InterfacesRemoved + * + * @param {string} object_path - Path interfaces have been removed from + * @param {string[]} interfaces - List of interface names removed + */ + _onInterfacesRemoved(object_path, interfaces) { + try { + // An empty interface list means the object is being removed + if (interfaces.length === 0) + return; + + // Get the proxy + const device = this._devices.get(object_path); + + if (device === undefined) + return; + + // Release the proxy and emit ::device-removed + this._devices.delete(object_path); + this.emit('device-removed', device); + + // Destroy the device and force disposal + device.destroy(); + } catch (e) { + logError(e, object_path); + } + } + + async _addDevices() { + const objects = await new Promise((resolve, reject) => { + this.call( + 'GetManagedObjects', + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + const variant = proxy.call_finish(res); + resolve(variant.deepUnpack()[0]); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + + for (const [object_path, object] of Object.entries(objects)) + await this._onInterfacesAdded(object_path, object); + } + + _clearDevices() { + for (const [object_path, device] of this._devices) { + this._devices.delete(object_path); + this.emit('device-removed', device); + device.destroy(); + } + } + + async _onNameOwnerChanged() { + try { + // If the service stopped, remove each device and mark it inactive + if (this.g_name_owner === null) { + this._clearDevices(); + + this._active = false; + this.notify('active'); + + // If the service started, mark it active and add each device + } else { + this._active = true; + this.notify('active'); + + await this._addDevices(); + } + } catch (e) { + logError(e); + } + } + + /** + * Reload all devices without affecting the remote service. This amounts to + * removing and adding each device while emitting the appropriate signals. + */ + async reload() { + try { + if (this._starting === false) { + this._starting = true; + + this._clearDevices(); + await _proxyInit(this); + await this._onNameOwnerChanged(); + + this._starting = false; + } + } catch (e) { + this._starting = false; + throw e; + } + } + + /** + * Start the service + */ + async start() { + try { + if (this._starting === false && this.active === false) { + this._starting = true; + + await _proxyInit(this); + await this._onNameOwnerChanged(); + + // Activate the service if it's not already running + if (!this.active) { + await new Promise((resolve, reject) => { + this.g_connection.call( + SERVICE_NAME, + SERVICE_PATH, + 'org.freedesktop.Application', + 'Activate', + GLib.Variant.new('(a{sv})', [{}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + (proxy, res) => { + try { + resolve(proxy.call_finish(res)); + } catch (e) { + Gio.DBusError.strip_remote_error(e); + reject(e); + } + } + ); + }); + } + + this._starting = false; + } + } catch (e) { + this._starting = false; + throw e; + } + } + + /** + * Stop the service + */ + stop() { + if (this.active) + this.activate_action('quit'); + } + + activate_action(name, parameter = null) { + try { + const paramArray = []; + + if (parameter instanceof GLib.Variant) + paramArray[0] = parameter; + + const connection = this.g_connection || Gio.DBus.session; + + connection.call( + SERVICE_NAME, + SERVICE_PATH, + 'org.freedesktop.Application', + 'ActivateAction', + GLib.Variant.new('(sava{sv})', [name, paramArray, {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null, + null + ); + } catch (e) { + logError(e); + } + } + + destroy() { + if (this._nameOwnerChangedId > 0) { + this.disconnect(this._nameOwnerChangedId); + this._nameOwnerChangedId = 0; + + this._clearDevices(); + this._active = false; + + GObject.signal_handlers_destroy(this); + } + } +}); diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.mjs b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.mjs new file mode 100644 index 0000000..90eb351 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/remote.mjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import GLib from 'gi://GLib'; + +const [filename] = GLib.filename_from_uri(import.meta.url); +const dirname = GLib.path_get_dirname(filename); +imports.searchPath.unshift(dirname); + +const Remote = imports.remote; +export default Remote; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.js new file mode 100644 index 0000000..d17963d --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.js @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; + +const Gettext = imports.gettext; + +const Config = imports.config; + + +/** + * Initialise and setup Gettext. + */ +function setupGettext() { + // 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); +} + +/** + * Initialise and setup Config, GResources and GSchema. + * @param {string} extensionPath - The absolute path to the extension directory + */ +function setup(extensionPath) { + // Ensure config.js is setup properly + Config.PACKAGE_DATADIR = extensionPath; + const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']); + + if (Config.PACKAGE_DATADIR.startsWith(userDir)) { + Config.IS_USER = true; + + Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`; + Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`; + } + + // 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 + ); +} diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.mjs b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.mjs new file mode 100644 index 0000000..7a14f19 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/utils/setup.mjs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import GLib from 'gi://GLib'; + +const [filename] = GLib.filename_from_uri(import.meta.url); +const dirname = GLib.path_get_dirname(filename); +imports.searchPath.unshift(dirname); + +export const {setup, setupGettext} = imports.setup; +export default setup; diff --git a/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/wl_clipboard.js b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/wl_clipboard.js new file mode 100644 index 0000000..2a11de5 --- /dev/null +++ b/gnome/.local/share/extensions/gsconnect@andyholmes.github.io/wl_clipboard.js @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: JingMatrix https://github.com/JingMatrix +// +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const Gio = imports.gi.Gio; +const GjsPrivate = imports.gi.GjsPrivate; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +// laucher for wl-clipboard +const launcher = new Gio.SubprocessLauncher({ + flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE, +}); + +/* + * DBus Interface Info + */ +const DBUS_NAME = 'org.gnome.Shell.Extensions.GSConnect.Clipboard'; +const DBUS_PATH = '/org/gnome/Shell/Extensions/GSConnect/Clipboard'; +const DBUS_NODE = Gio.DBusNodeInfo.new_for_xml(` +<node> + <interface name="org.gnome.Shell.Extensions.GSConnect.Clipboard"> + <!-- Methods --> + <method name="GetMimetypes"> + <arg direction="out" type="as" name="mimetypes"/> + </method> + <method name="GetText"> + <arg direction="out" type="s" name="text"/> + </method> + <method name="SetText"> + <arg direction="in" type="s" name="text"/> + </method> + + <!-- Signals --> + <signal name="OwnerChange"/> + </interface> +</node> +`); +const DBUS_INFO = DBUS_NODE.lookup_interface(DBUS_NAME); + +/* + * Text Mimetypes + */ +const TEXT_MIMETYPES = [ + 'text/plain;charset=utf-8', + 'UTF8_STRING', + 'text/plain', + 'STRING', +]; + +/* GSConnectClipboardPortal: + * + * A simple clipboard portal, especially useful on Wayland where GtkClipboard + * doesn't work in the background. + */ +var Clipboard = GObject.registerClass( + { + GTypeName: 'GSConnectShellClipboard', + }, + class GSConnectShellClipboard extends GjsPrivate.DBusImplementation { + _init() { + super._init({ + g_interface_info: DBUS_INFO, + }); + + this._transferring = false; + + this.watcher = Gio.Subprocess.new([ + 'wl-paste', + '-w', + 'dbus-send', + DBUS_PATH, + '--dest=' + DBUS_NAME, + DBUS_NAME + '.OwnerChange', + ], Gio.SubprocessFlags.NONE); + + // Prepare DBus interface + this._handleMethodCallId = this.connect( + 'handle-method-call', + this._onHandleMethodCall.bind(this) + ); + + this._nameId = Gio.DBus.own_name( + Gio.BusType.SESSION, + DBUS_NAME, + Gio.BusNameOwnerFlags.NONE, + this._onBusAcquired.bind(this), + null, + this._onNameLost.bind(this) + ); + } + + _onBusAcquired(connection, name) { + try { + this.export(connection, DBUS_PATH); + } catch (e) { + logError(e); + } + } + + _onNameLost(connection, name) { + try { + this.unexport(); + } catch (e) { + logError(e); + } + } + + async _onHandleMethodCall(iface, name, parameters, invocation) { + let retval; + + try { + const args = parameters.recursiveUnpack(); + + retval = await this[name](...args); + } catch (e) { + if (e instanceof GLib.Error) { + invocation.return_gerror(e); + } else { + if (!e.name.includes('.')) + e.name = `org.gnome.gjs.JSError.${e.name}`; + + invocation.return_dbus_error(e.name, e.message); + } + + return; + } + + if (retval === undefined) + retval = new GLib.Variant('()', []); + + try { + if (!(retval instanceof GLib.Variant)) { + const args = DBUS_INFO.lookup_method(name).out_args; + retval = new GLib.Variant( + `(${args.map((arg) => arg.signature).join('')})`, + args.length === 1 ? [retval] : retval + ); + } + + invocation.return_value(retval); + + // Without a response, the client will wait for timeout + } catch (e) { + invocation.return_dbus_error( + 'org.gnome.gjs.JSError.ValueError', + 'Service implementation returned an incorrect value type' + ); + } + } + + /** + * Get the available mimetypes of the current clipboard content + * + * @return {Promise<string[]>} A list of mime-types + */ + + GetMimetypes() { + return new Promise((resolve, reject) => { + const proc = launcher.spawnv([ + 'wl-paste', + '--list-types', + '-n', + ]); + proc.communicate_utf8_async(null, null, (proc, res) => { + try { + const [, stdout, stderr] = + proc.communicate_utf8_finish(res); + if (proc.get_successful()) + resolve(stdout.trim().split('\n')); + else + logError(stderr); + + } catch (e) { + reject(e); + } + }); + }); + } + + /** + * Get the text content of the clipboard + * + * @return {Promise<string>} Text content of the clipboard + */ + GetText() { + return new Promise((resolve, reject) => { + this.GetMimetypes().then((mimetypes) => { + const mimetype = TEXT_MIMETYPES.find((type) => + mimetypes.includes(type) + ); + + if (mimetype !== undefined) { + const proc = launcher.spawnv(['wl-paste', '-n']); + proc.communicate_utf8_async(null, null, (proc, res) => { + try { + const [, stdout, stderr] = + proc.communicate_utf8_finish(res); + if (proc.get_successful()) + resolve(stdout); + else + logError(stderr); + + } catch (e) { + reject(e); + } + }); + } else { + reject(new Error('text not available')); + } + }); + }); + } + + /** + * Set the text content of the clipboard + * + * @param {string} text - text content to set + * @return {Promise} A promise for the operation + */ + SetText(text) { + return new Promise((resolve, reject) => { + try { + if (typeof text !== 'string') { + throw new Gio.DBusError({ + code: Gio.DBusError.INVALID_ARGS, + message: 'expected string', + }); + } + + launcher.spawnv(['wl-copy', text]); + resolve(); + } catch (e) { + reject(e); + } + }); + } + + + + destroy() { + if (this._nameId > 0) { + Gio.bus_unown_name(this._nameId); + this._nameId = 0; + } + + if (this._handleMethodCallId > 0) { + this.disconnect(this._handleMethodCallId); + this._handleMethodCallId = 0; + this.unexport(); + } + if (this.watcher) + this.watcher.force_exit(); + + } + } +); + +var _portal = null; +var _portalId = 0; + +/** + * Watch for the service to start and export the clipboard portal when it does. + */ +function watchService() { + if (_portalId > 0) + return; + + _portalId = Gio.bus_watch_name( + Gio.BusType.SESSION, + 'org.gnome.Shell.Extensions.GSConnect', + Gio.BusNameWatcherFlags.NONE, + () => { + if (_portal === null) + _portal = new Clipboard(); + }, + () => { + if (_portal !== null) { + _portal.destroy(); + _portal = null; + } + } + ); +} + +/** + * Stop watching the service and export the portal if currently running. + */ +function unwatchService() { + if (_portalId > 0) { + Gio.bus_unwatch_name(_portalId); + _portalId = 0; + } +} + +// vim:tabstop=2:shiftwidth=2:expandtab