extension.js 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. // Bing Wallpaper GNOME extension
  2. // Copyright (C) 2017-2023 Michael Carroll
  3. // This extension is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Lesser General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. // See the GNU General Public License, version 3 or later for details.
  8. // Based on GNOME shell extension NASA APOD by Elia Argentieri https://github.com/Elinvention/gnome-shell-extension-nasa-apod
  9. import St from 'gi://St';
  10. import Soup from 'gi://Soup';
  11. import Gio from 'gi://Gio';
  12. import GObject from 'gi://GObject';
  13. import GLib from 'gi://GLib';
  14. import Clutter from 'gi://Clutter';
  15. import Cogl from 'gi://Cogl';
  16. import * as Main from 'resource:///org/gnome/shell/ui/main.js';
  17. import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
  18. import {Button} from 'resource:///org/gnome/shell/ui/panelMenu.js';
  19. import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
  20. import * as Config from 'resource:///org/gnome/shell/misc/config.js';
  21. import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
  22. import * as Utils from './utils.js';
  23. import Blur from './blur.js';
  24. import Thumbnail from './thumbnail.js';
  25. import BWClipboard from './BWClipboard.js';
  26. const BingImageURL = Utils.BingImageURL;
  27. const BingURL = 'https://www.bing.com';
  28. const IndicatorName = 'BingWallpaperIndicator';
  29. const TIMEOUT_SECONDS = 24 * 3600; // FIXME: this should use the end data from the json data
  30. const TIMEOUT_SECONDS_ON_HTTP_ERROR = 1 * 3600; // retry in one hour if there is a http error3
  31. const MINIMUM_SHUFFLE_IMAGES = 3; // bare minimum to use filtered image set in shuffle mode
  32. const ICON_PREVIOUS_BUTTON = 'media-seek-backward-symbolic';
  33. const ICON_NEXT_BUTTON = 'media-seek-forward-symbolic';
  34. const ICON_CURRENT_BUTTON = 'media-skip-forward-symbolic';
  35. let bingWallpaperIndicator = null;
  36. let blur = null;
  37. const newMenuItem = (label) => {
  38. return new PopupMenu.PopupMenuItem(label);
  39. }
  40. const newMenuSwitchItem = (label, state) => {
  41. let switchItem = new PopupMenu.PopupSwitchMenuItem(
  42. label,
  43. state,
  44. {});
  45. switchItem.label.x_expand = true;
  46. switchItem._statusBin.x_expand = false;
  47. return switchItem;
  48. }
  49. function log(msg) {
  50. if (BingDebug())
  51. console.log('BingWallpaper extension: ' + msg); // disable to keep the noise down in journal
  52. }
  53. function BingDebug() {
  54. if (bingWallpaperIndicator && bingWallpaperIndicator._settings.get_boolean('debug-logging'))
  55. return true;
  56. return false;
  57. }
  58. function notifyError(msg) {
  59. Main.notifyError("BingWallpaper extension error", msg);
  60. }
  61. function doSetBackground(uri, schema) {
  62. let gsettings = new Gio.Settings({schema: schema});
  63. let prev = gsettings.get_string('picture-uri');
  64. uri = 'file://' + uri;
  65. gsettings.set_string('picture-uri', uri);
  66. try {
  67. gsettings.set_string('picture-uri-dark', uri);
  68. }
  69. catch (e) {
  70. log("unable to set dark background for : " + e);
  71. }
  72. Gio.Settings.sync();
  73. gsettings.apply();
  74. return (prev != uri); // return true if background uri has changed
  75. }
  76. const BingWallpaperIndicator = GObject.registerClass(
  77. class BingWallpaperIndicator extends Button {
  78. _init(ext) {
  79. super._init(0, IndicatorName, false);
  80. this.title = "";
  81. this.explanation = "";
  82. this.filename = "";
  83. this.copyright = "";
  84. this.version = "0.1";
  85. this._updatePending = false;
  86. this._timeout = null;
  87. this._shuffleTimeout = null;
  88. this.longstartdate = null;
  89. this.imageURL = ""; // link to image itself
  90. this.imageinfolink = ""; // link to Bing photo info page
  91. this.refreshdue = 0;
  92. this.shuffledue = 0;
  93. this.refreshduetext = "";
  94. this.thumbnail = null;
  95. this.thumbnailItem = null;
  96. this.selected_image = "current";
  97. this.clipboard = new BWClipboard();
  98. this.imageIndex = null;
  99. this.logger = null;
  100. this.favourite_status = false;
  101. this.hidden_status = false;
  102. this.dimensions = { 'width': null, 'height': null};
  103. this._extension = ext;
  104. let extensionIconsPath = ext.dir.get_child('icons').get_path();
  105. this.ICON_RANDOM = extensionIconsPath + '/'+'game-die-symbolic.svg';
  106. this.ICON_FAVE_BUTTON = extensionIconsPath + '/'+'fav-symbolic.svg';
  107. this.ICON_UNFAVE_BUTTON = extensionIconsPath + '/'+'unfav-symbolic.svg';
  108. this.ICON_TRASH_BUTTON = extensionIconsPath + '/'+'trash-empty-symbolic.svg';
  109. this.ICON_UNTRASH_BUTTON = extensionIconsPath + '/'+'trash-full-symbolic.svg';
  110. if (!blur) // as Blur isn't disabled on screen lock (like the rest of the extension is)
  111. blur = new Blur();
  112. // take a variety of actions when the gsettings values are modified by prefs
  113. this._settings = this._extension.getSettings();
  114. // create Soup session
  115. this._initSoup();
  116. this.visible = !this._settings.get_boolean('hide');
  117. this.refreshDueItem = newMenuItem(_("<No refresh scheduled>"));
  118. this.explainItem = newMenuItem(_("Awaiting refresh..."));
  119. this.copyrightItem = newMenuItem(_("Awaiting refresh..."));
  120. this.clipboardImageItem = newMenuItem(_("Copy image to clipboard"));
  121. this.clipboardURLItem = newMenuItem(_("Copy image URL to clipboard"));
  122. this.folderItem = newMenuItem(_("Open image folder"));
  123. this.dwallpaperItem = newMenuItem(_("Set background image"));
  124. this.swallpaperItem = newMenuItem(_("Set lock screen image"));
  125. this.refreshItem = newMenuItem(_("Refresh Now"));
  126. this.settingsItem = newMenuItem(_("Settings"));
  127. this.openImageItem = newMenuItem(_("Open in image viewer"));
  128. this.openImageInfoLinkItem = newMenuItem(_("Open Bing image information page"));
  129. this.imageResolutionItem = newMenuItem(_("Awaiting refresh..."));
  130. this.titleItem = new PopupMenu.PopupSubMenuMenuItem(_("Awaiting refresh..."), false);
  131. [this.imageResolutionItem, this.openImageInfoLinkItem, this.openImageItem, this.folderItem,
  132. this.clipboardImageItem, this.clipboardURLItem, this.dwallpaperItem]
  133. .forEach(e => this.titleItem.menu.addMenuItem(e));
  134. // quick settings submenu
  135. this.settingsSubMenu = new PopupMenu.PopupSubMenuMenuItem(_("Quick settings"), false);
  136. // toggles under the quick settings submenu
  137. this.toggleSetBackground = newMenuSwitchItem(_("Set background image"), this._settings.get_boolean('set-background'));
  138. this.toggleSelectNew = newMenuSwitchItem(_("Always show new images"), this._settings.get_boolean('revert-to-current-image'));
  139. this.toggleShuffle = newMenuSwitchItem(_("Image shuffle mode"), true);
  140. this.toggleShuffleOnlyFaves = newMenuSwitchItem(_("Image shuffle only favorites"), this._settings.get_boolean('random-mode-include-only-favourites'));
  141. /*this.toggleNotifications = newMenuSwitchItem(_("Enable desktop notifications"), this._settings.get_boolean('notify'));*/
  142. this.toggleImageCount = newMenuSwitchItem(_("Show image count"), this._settings.get_boolean('show-count-in-image-title'));
  143. this.toggleShuffleOnlyUHD = newMenuSwitchItem(_("Image shuffle only UHD resolutions"), this._settings.get_boolean('random-mode-include-only-uhd'));
  144. [/*this.toggleNotifications, this.toggleImageCount, this.toggleSetBackground,*/ this.toggleSelectNew,
  145. this.toggleShuffle, this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD]
  146. .forEach(e => this.settingsSubMenu.menu.addMenuItem(e));
  147. // these items are a bit unique, we'll populate them in _setControls()
  148. this.controlItem = newMenuItem("");
  149. this.thumbnailItem = new PopupMenu.PopupBaseMenuItem({ style_class: 'wp-thumbnail-image'});
  150. this._setControls(); // build the button bar
  151. // we need to word-wrap these menu items to not overflow menu in case of long lines of text
  152. [this.refreshDueItem, this.titleItem, this.explainItem, this.copyrightItem]
  153. .forEach((e, i) => {
  154. this._wrapLabelItem(e);
  155. });
  156. // set the order of menu items (including separators)
  157. let allMenuItems = [
  158. this.refreshItem,
  159. this.refreshDueItem,
  160. new PopupMenu.PopupSeparatorMenuItem(),
  161. this.controlItem,
  162. new PopupMenu.PopupSeparatorMenuItem(),
  163. this.explainItem,
  164. this.thumbnailItem,
  165. this.titleItem,
  166. this.copyrightItem,
  167. new PopupMenu.PopupSeparatorMenuItem(),
  168. this.settingsSubMenu,
  169. this.settingsItem
  170. ];
  171. allMenuItems.forEach(e => this.menu.addMenuItem(e));
  172. // non clickable information items
  173. [this.explainItem, this.copyrightItem, this.refreshDueItem, this.thumbnailItem, this.imageResolutionItem]
  174. .forEach((e) => {
  175. e.setSensitive(false);
  176. });
  177. if (this._settings.get_boolean('random-mode-enabled')) {
  178. [this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD]
  179. .forEach((e) => {
  180. e.setSensitive(false);
  181. });
  182. }
  183. this._setConnections();
  184. if (this._settings.get_string('state') != '[]') { // setting state on reset or initial boot
  185. this._reStoreState();
  186. }
  187. else {
  188. this._restartTimeout(60); // wait 60 seconds before performing refresh
  189. }
  190. }
  191. // create Soup session
  192. _initSoup() {
  193. this.httpSession = new Soup.Session();
  194. 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;
  195. }
  196. // listen for configuration changes
  197. _setConnections() {
  198. this.settings_connections = [];
  199. this.settings_connections.push(
  200. this._settings.connect('changed::hide', () => {
  201. this.visible = !this._settings.get_boolean('hide');
  202. })
  203. );
  204. let settingConnections = [
  205. {signal: 'changed::icon-name', call: this._setIcon},
  206. {signal: 'changed::market', call: this._refresh},
  207. {signal: 'changed::set-background', call: this._setBackground},
  208. {signal: 'changed::override-lockscreen-blur', call: this._setBlur},
  209. {signal: 'changed::lockscreen-blur-strength', call: this._setBlur},
  210. {signal: 'changed::lockscreen-blur-brightness', call: this._setBlur},
  211. {signal: 'changed::selected-image', call: this._setImage},
  212. {signal: 'changed::delete-previous', call: this._cleanUpImages},
  213. {signal: 'changed::notify', call: this._notifyCurrentImage},
  214. {signal: 'changed::always-export-bing-json', call: this._exportData},
  215. {signal: 'changed::bing-json', call: this._exportData},
  216. {signal: 'changed::controls-icon-size', call: this._setControls},
  217. {signal: 'changed::random-mode-enabled', call: this._randomModeChanged},
  218. {signal: 'changed::random-mode-include-only-favourites', call: this._randomModeChanged},
  219. {signal: 'changed::random-mode-include-only-unhidden', call: this._randomModeChanged},
  220. {signal: 'changed::random-mode-include-only-uhd', call: this._randomModeChanged},
  221. {signal: 'changed::random-interval-mode', call: this._randomModeChanged}
  222. ];
  223. // _setShuffleToggleState
  224. settingConnections.forEach((e) => {
  225. this.settings_connections.push(
  226. this._settings.connect(e.signal, e.call.bind(this))
  227. );
  228. });
  229. // ensure we're in a sensible initial state
  230. this._setIcon();
  231. this._setBlur();
  232. this._setImage();
  233. this._cleanUpImages();
  234. // menu connections
  235. this.connect('button-press-event', this._openMenu.bind(this));
  236. // link menu items to functions
  237. //this.thumbnailItem.connect('activate', this._setBackgroundDesktop.bind(this));
  238. this.thumbnailItem.connect('activate', this._openInSystemViewer.bind(this));
  239. this.openImageItem.connect('activate', this._openInSystemViewer.bind(this));
  240. //this.titleItem.connect('activate', this._setBackgroundDesktop.bind(this));
  241. this.openImageInfoLinkItem.connect('activate', this._openImageInfoLink.bind(this));
  242. this.dwallpaperItem.connect('activate', this._setBackgroundDesktop.bind(this));
  243. this.refreshItem.connect('activate', this._refresh.bind(this));
  244. this.settingsItem.connect('activate', this._openPrefs.bind(this));
  245. // unfortunately we can't bind toggles trivially like we can with prefs.js here, so we handle toggles in two steps
  246. // first, we listen for changes to these toggle settings and update the status
  247. // & then, link settings to toggle state (the other way)
  248. let toggles = [ /*{key: 'set-background', toggle: this.toggleSetBackground},*/
  249. {key: 'revert-to-current-image', toggle: this.toggleSelectNew},
  250. /*{key: 'notify', toggle: this.toggleNotifications},
  251. {key: 'show-count-in-image-title', toggle: this.toggleImageCount},*/
  252. {key: 'random-mode-enabled', toggle: this.toggleShuffle},
  253. {key: 'random-mode-include-only-favourites', toggle: this.toggleShuffleOnlyFaves},
  254. /*{key: 'random-mode-include-only-unhidden', toggle: this.toggleShuffleOnlyUnhidden},*/
  255. {key: 'random-mode-include-only-uhd', toggle: this.toggleShuffleOnlyUHD}];
  256. toggles.forEach( (e) => {
  257. this.settings_connections.push(
  258. this._settings.connect('changed::'+e.key, () => {
  259. log(e.key+' setting changed to '+ (this._settings.get_boolean(e.key)?'true':'false'));
  260. e.toggle.setToggleState(this._settings.get_boolean(e.key));
  261. })
  262. );
  263. e.toggle.connect('toggled', (item, state) => {
  264. log(e.key+' switch toggled to '+ (state?'true':'false'));
  265. this._setBooleanSetting(e.key, state);
  266. });
  267. });
  268. this.folderItem.connect('activate', Utils.openImageFolder.bind(this, this._settings));
  269. if (this.clipboard.clipboard) { // only if we have a clipboard
  270. this.clipboardImageItem.connect('activate', this._copyImageToClipboard.bind(this));
  271. this.clipboardURLItem.connect('activate', this._copyURLToClipboard.bind(this));
  272. }
  273. else {
  274. [this.clipboardImageItem, this.clipboardURLItem].
  275. forEach(e => e.setSensitive(false));
  276. }
  277. }
  278. _setBooleanSetting(key, state) {
  279. let success = this._settings.set_boolean(key, state);
  280. log('key '+key+' set to ' + (state?'true':'false') + ' (returned ' + (success?'true':'false')+')');
  281. }
  282. _setStringSetting(key, value) {
  283. let success = this._settings.set_string(key, value);
  284. log('key '+key+' set to ' + value + ' (returned ' + (success?'true':'false')+')');
  285. }
  286. _setIntSetting(key, value) {
  287. let success = this._settings.set_int(key, value);
  288. log('key '+key+' set to ' + value + ' (returned ' + (success?'true':'false')+')');
  289. }
  290. _onDestroy() {
  291. this._unsetConnections();
  292. }
  293. _unsetConnections() {
  294. this.settings_connections.forEach((e) => {
  295. this._settings.disconnect(e);
  296. });
  297. }
  298. _openPrefs() {
  299. this._extension.openPreferences();
  300. }
  301. _openMenu() {
  302. // Grey out menu items if an update is pending
  303. this.refreshItem.setSensitive(!this._updatePending);
  304. this.clipboardImageItem.setSensitive(!this._updatePending && this.imageURL != "");
  305. this.clipboardURLItem.setSensitive(!this._updatePending && this.imageURL != "");
  306. this.thumbnailItem.setSensitive(!this._updatePending && this.imageURL != "");
  307. this.dwallpaperItem.setSensitive(!this._updatePending && this.filename != "");
  308. this.swallpaperItem.setSensitive(!this._updatePending && this.filename != "");
  309. this.titleItem.setSensitive(!this._updatePending && this.imageinfolink != "");
  310. let maxlongdate = Utils.getMaxLongDate(this._settings);
  311. this.refreshduetext =
  312. _("Next refresh") + ": " + (this.refreshdue ? this.refreshdue.format("%Y-%m-%d %X") : '-') +
  313. " (" + Utils.friendly_time_diff(this.refreshdue) + ")\n" +
  314. _("Last refresh") + ": " + (maxlongdate? this._localeDate(maxlongdate, true) : '-');
  315. // also show when shuffle is next due
  316. if (this._settings.get_boolean('random-mode-enabled')) {
  317. this.refreshduetext += "\n" + _("Next shuffle")+": " +
  318. (this.shuffledue ? this.shuffledue.format("%Y-%m-%d %X") : '-') +
  319. " (" + Utils.friendly_time_diff(this.shuffledue) + ")";
  320. }
  321. this.refreshDueItem.label.set_text(this.refreshduetext);
  322. }
  323. _setBlur() {
  324. blur._switch(this._settings.get_boolean('override-lockscreen-blur'));
  325. blur.set_blur_strength(this._settings.get_int('lockscreen-blur-strength'));
  326. blur.set_blur_brightness(this._settings.get_int('lockscreen-blur-brightness'));
  327. }
  328. _setImage() {
  329. Utils.validate_imagename(this._settings);
  330. this.selected_image = this._settings.get_string('selected-image');
  331. log('selected image changed to: ' + this.selected_image);
  332. this._selectImage();
  333. //this._setShuffleToggleState();
  334. }
  335. _notifyCurrentImage() {
  336. if (this._settings.get_boolean('notify')) {
  337. let image = this._getCurrentImage();
  338. if (image) {
  339. this._createImageNotification(image);
  340. }
  341. }
  342. }
  343. // set indicator icon (tray icon)
  344. _setIcon() {
  345. Utils.validate_icon(this._settings, this._extension.path);
  346. let icon_name = this._settings.get_string('icon-name');
  347. let gicon = Gio.icon_new_for_string(this._extension.dir.get_child('icons').get_path() + '/' + icon_name + '.svg');
  348. this.icon = new St.Icon({gicon: gicon, style_class: 'system-status-icon'});
  349. log('Replace icon set to: ' + icon_name);
  350. this.remove_all_children();
  351. this.add_child(this.icon);
  352. }
  353. // set backgrounds as requested and set preview image in menu
  354. _setBackground() {
  355. if (this.filename == '')
  356. return;
  357. this.thumbnail = new Thumbnail(this.filename, St.ThemeContext.get_for_stage(global.stage).scale_factor); // use scale factor to make them look nicer
  358. this._setThumbnailImage();
  359. if (!this.dimensions.width || !this.dimensions.height) // if dimensions aren't in image database yet
  360. [this.dimensions.width, this.dimensions.height] = Utils.getFileDimensions(this.filename);
  361. log('image set to : '+this.filename);
  362. if (this._settings.get_boolean('set-background'))
  363. this._setBackgroundDesktop();
  364. }
  365. _setBackgroundDesktop() {
  366. doSetBackground(this.filename, Utils.DESKTOP_SCHEMA);
  367. }
  368. _copyURLToClipboard() {
  369. this.clipboard.setText(this.imageURL);
  370. }
  371. _copyImageToClipboard() {
  372. this.clipboard.setImage(this.filename);
  373. }
  374. // set a timer on when the current image is going to expire
  375. _restartTimeoutFromLongDate(longdate) {
  376. // all Bing times are in UTC (+0)
  377. let refreshDue = Utils.dateFromLongDate(longdate, 86400).to_local();
  378. let now = GLib.DateTime.new_now_local();
  379. let difference = refreshDue.difference(now) / 1000000;
  380. if (difference < 60 || difference > 86400) // clamp to a reasonable range
  381. difference = 60;
  382. difference = difference + 300; // 5 minute fudge offset in case of inaccurate local clock
  383. log('Next refresh due ' + difference + ' seconds from now');
  384. this._restartTimeout(difference);
  385. }
  386. // alternative shuffle mode, not yet enabled
  387. _restartShuffleTimeoutFromDueDate(duedate) {
  388. let now = GLib.DateTime.new_now_local();
  389. let difference = duedate.difference(now) / 1000000;
  390. if (difference < 60 || difference > 86400) // clamp to a reasonable range
  391. difference = 60;
  392. log('Next shuffle due ' + difference + ' seconds from now');
  393. this._restartShuffleTimeout(difference);
  394. }
  395. // convert longdate format into human friendly format
  396. _localeDate(longdate, include_time = false) {
  397. try {
  398. let date = Utils.dateFromLongDate(longdate, 300); // date at update
  399. return date.to_local().format('%Y-%m-%d' + (include_time? ' %X' : '')); // ISO 8601 - https://xkcd.com/1179/
  400. }
  401. catch (e) {
  402. return 'none';
  403. }
  404. }
  405. // set menu text in lieu of a notification/popup
  406. _setMenuText() {
  407. this.titleItem.label.set_text(this.title ? this.title : '');
  408. this.copyrightItem.label.set_text(this.copyright ? this.copyright : '');
  409. this.imageResolutionItem.label.set_text(this.dimensions.width+'px x '+this.dimensions.height+'px');
  410. if (this._settings.get_boolean('show-count-in-image-title') && this.explanation) {
  411. let imageList = JSON.parse(this._settings.get_string('bing-json'));
  412. if (imageList.length > 0)
  413. this.explainItem.label.set_text( this.explanation + ' [' + (this.imageIndex + 1) + '/' + imageList.length + ']');
  414. }
  415. else {
  416. this.explainItem.label.set_text(this.explanation ? this.explanation : '');
  417. }
  418. this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON);
  419. this._setTrashIcon(this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON);
  420. }
  421. _wrapLabelItem(menuItem) {
  422. let clutter_text = menuItem.label.get_clutter_text();
  423. clutter_text.set_line_wrap(true);
  424. clutter_text.set_ellipsize(0);
  425. clutter_text.set_max_length(0);
  426. menuItem.label.set_style('max-width: 420px;');
  427. }
  428. _setControls() {
  429. this.favouriteBtn = this._newMenuIcon(
  430. this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON,
  431. this.controlItem,
  432. this._favouriteImage);
  433. this.trashBtn = this._newMenuIcon(
  434. this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON,
  435. this.controlItem,
  436. this._trashImage);
  437. this.prevBtn = this._newMenuIcon(
  438. ICON_PREVIOUS_BUTTON,
  439. this.controlItem,
  440. this._prevImage);
  441. this.nextBtn = this._newMenuIcon(
  442. ICON_NEXT_BUTTON,
  443. this.controlItem,
  444. this._nextImage);
  445. this.curBtn = this._newMenuIcon(
  446. ICON_CURRENT_BUTTON,
  447. this.controlItem,
  448. this._curImage);
  449. this.randomizeBtn = this._newMenuIcon(
  450. this.ICON_RANDOM,
  451. this.controlItem,
  452. this._selectImage,
  453. null, true);
  454. }
  455. _newMenuIcon(icon_name, parent, fn, position = null, arg = null) {
  456. let gicon = Gio.icon_new_for_string(icon_name);
  457. let icon = new St.Icon({
  458. /*icon_name: icon_name,*/
  459. gicon: gicon,
  460. style_class: 'popup-menu-icon',
  461. x_expand: true,
  462. y_expand: true,
  463. icon_size: this._settings.get_int('controls-icon-size')
  464. });
  465. let iconBtn = new St.Button({
  466. style_class: 'ci-action-btn',
  467. can_focus: true,
  468. child: icon,
  469. /* x_align: Clutter.ActorAlign.END, // FIXME: errors on GNOME 3.28, default to center is ok */
  470. x_expand: true,
  471. y_expand: true
  472. });
  473. if (position !== null) {
  474. parent.insert_child_at_index(iconBtn, position);
  475. }
  476. else {
  477. parent.add_child(iconBtn);
  478. }
  479. iconBtn.connect('button-press-event', fn.bind(this, arg));
  480. return iconBtn;
  481. }
  482. // set menu thumbnail
  483. _setThumbnailImage() {
  484. let pixbuf = this.thumbnail.pixbuf;
  485. let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
  486. if (pixbuf == null)
  487. return;
  488. const {width, height} = pixbuf;
  489. if (height == 0) {
  490. return;
  491. }
  492. const image = new Clutter.Image();
  493. const success = image.set_data(
  494. pixbuf.get_pixels(),
  495. pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888,
  496. width,
  497. height,
  498. pixbuf.get_rowstride()
  499. );
  500. if (!success) {
  501. throw Error("error creating Clutter.Image()");
  502. }
  503. this.thumbnailItem.hexpand = false;
  504. this.thumbnailItem.vexpand = false;
  505. this.thumbnailItem.content = image;
  506. log('scale factor: ' + scale_factor);
  507. this.thumbnailItem.set_size(480*scale_factor, 270*scale_factor);
  508. this.thumbnailItem.setSensitive(true);
  509. }
  510. _nextImage() {
  511. this._gotoImage(1);
  512. }
  513. _prevImage() {
  514. this._gotoImage(-1);
  515. }
  516. _curImage() {
  517. this._setStringSetting('selected-image', 'current');
  518. this._gotoImage(0);
  519. }
  520. _randomModeChanged() {
  521. let randomEnabled = this._settings.get_boolean('random-mode-enabled');
  522. Utils.validate_interval(this._settings);
  523. [this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD /*, this.toggleShuffleOnlyUnhidden*/]
  524. .forEach( x => {
  525. x.setSensitive(randomEnabled);
  526. });
  527. if (randomEnabled) {
  528. log('enabled shuffle mode, by setting a shuffe timer (5 seconds)');
  529. this._restartShuffleTimeout(5);
  530. this._setBooleanSetting('revert-to-current-image', false);
  531. }
  532. else {
  533. // clear shuffle timer
  534. if (this._shuffleTimeout)
  535. GLib.source_remove(this._shuffleTimeout);
  536. this._setBooleanSetting('revert-to-current-image', true);
  537. }
  538. }
  539. _favouriteImage() {
  540. log('favourite image '+this.imageURL+' status was '+this.favourite_status);
  541. this.favourite_status = !this.favourite_status;
  542. Utils.setImageFavouriteStatus(this._settings, this.imageURL, this.favourite_status);
  543. this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON);
  544. }
  545. _trashImage() {
  546. log('trash image '+this.imageURL+' status was '+this.hidden_status);
  547. this.hidden_status = !this.hidden_status;
  548. Utils.setImageHiddenStatus(this._settings, this.imageURL, this.hidden_status);
  549. this._setTrashIcon(this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON);
  550. if (this._settings.get_boolean('trash-deletes-images')) {
  551. log('image to be deleted: '+this.filename);
  552. Utils.deleteImage(this.filename);
  553. Utils.validate_imagename(this._settings);
  554. }
  555. }
  556. _setFavouriteIcon(icon_name) {
  557. let gicon = Gio.icon_new_for_string(icon_name);
  558. this.favouriteBtn.get_children().forEach( (x, i) => {
  559. x.set_gicon(gicon);
  560. });
  561. }
  562. _setTrashIcon(icon_name) {
  563. let gicon = Gio.icon_new_for_string(icon_name);
  564. this.trashBtn.get_children().forEach( (x, i) => {
  565. x.set_gicon(gicon);
  566. });
  567. }
  568. _gotoImage(relativePos) {
  569. let imageList = Utils.getImageList(this._settings);
  570. let curIndex = 0;
  571. if (this.selected_image == 'current') {
  572. curIndex = Utils.getCurrentImageIndex(imageList);
  573. }
  574. else {
  575. curIndex = Utils.imageIndex(imageList, this.selected_image);
  576. }
  577. let newImage = Utils.getImageByIndex(imageList, curIndex + relativePos);
  578. if (newImage)
  579. this._setStringSetting('selected-image', newImage.urlbase.replace('/th?id=OHR.', ''));
  580. }
  581. _getCurrentImage() {
  582. let imageList = Utils.getImageList(this._settings);
  583. let curIndex = Utils.getCurrentImageIndex(imageList);
  584. return Utils.getImageByIndex(imageList, curIndex);
  585. }
  586. // download Bing metadata
  587. _refresh() {
  588. if (this._updatePending)
  589. return;
  590. this._updatePending = true;
  591. this._restartTimeout();
  592. let market = this._settings.get_string('market');
  593. // Soup3 should be the version used, but in the past some distros have packaged older versions only
  594. if (Soup.MAJOR_VERSION >= 3) {
  595. let url = BingImageURL;
  596. let params = Utils.BingParams;
  597. params['mkt'] = ( market != 'auto' ? market : '' );
  598. // if we've set previous days to be something less than 8 and
  599. // delete previous is active we want to just request a subset of wallpapers
  600. if (this._settings.get_boolean('delete-previous') == true && this._settings.get_int('previous-days')<8) {
  601. params['n'] = ""+this._settings.get_int('previous-days');
  602. }
  603. let request = Soup.Message.new_from_encoded_form('GET', url, Soup.form_encode_hash(params));
  604. request.request_headers.append('Accept', 'application/json');
  605. try {
  606. this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => {
  607. this._processMessageRefresh(message);
  608. });
  609. }
  610. catch(error) {
  611. log('unable to send libsoup json message '+error);
  612. notifyError('Unable to fetch Bing metadata\n'+error);
  613. }
  614. }
  615. else {
  616. let url = BingImageURL + '?format=js&idx=0&n=8&mbl=1&mkt=' + (market != 'auto' ? market : '');
  617. let request = Soup.Message.new('GET', url);
  618. request.request_headers.append('Accept', 'application/json');
  619. // queue the http request
  620. try {
  621. this.httpSession.queue_message(request, (httpSession, message) => {
  622. this._processMessageRefresh(message);
  623. });
  624. }
  625. catch (error) {
  626. log('unable to send libsoup json message '+error);
  627. notifyError('Unable to fetch Bing metadata\n'+error);
  628. }
  629. }
  630. }
  631. _processMessageRefresh(message) {
  632. const decoder = new TextDecoder();
  633. try {
  634. let data = (Soup.MAJOR_VERSION >= 3) ?
  635. decoder.decode(this.httpSession.send_and_read_finish(message).get_data()): // Soup3
  636. message.response_body.data; // Soup 2
  637. log('Recieved ' + data.length + ' bytes');
  638. this._parseData(data);
  639. if (!this._settings.get_boolean('random-mode-enabled'))
  640. this._selectImage();
  641. }
  642. catch (error) {
  643. log('Network error occured: ' + error);
  644. notifyError('network error occured\n'+error);
  645. this._updatePending = false;
  646. this._restartTimeout(TIMEOUT_SECONDS_ON_HTTP_ERROR);
  647. }
  648. }
  649. // sets a timer for next refresh of Bing metadata
  650. _restartTimeout(seconds = null) {
  651. if (this._timeout)
  652. GLib.source_remove(this._timeout);
  653. if (seconds == null)
  654. seconds = TIMEOUT_SECONDS;
  655. this._timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._refresh.bind(this));
  656. this.refreshdue = GLib.DateTime.new_now_local().add_seconds(seconds);
  657. log('next check in ' + seconds + ' seconds');
  658. }
  659. _restartShuffleTimeout(seconds = null) {
  660. log('_restartShuffleTimeout('+seconds+')');
  661. //console.trace();
  662. if (this._shuffleTimeout)
  663. GLib.source_remove(this._shuffleTimeout);
  664. if (seconds == null) {
  665. let diff = -Math.floor(GLib.DateTime.new_now_local().difference(this.shuffledue)/1000000);
  666. log('shuffle ('+this.shuffledue.format_iso8601()+') diff = '+diff);
  667. if (diff > 30) { // on occasions the above will be 1 second
  668. seconds = diff; // if not specified, we should maintain the existing shuffle timeout (i.e. we just restored from saved state)
  669. }
  670. else if (this._settings.get_string('random-interval-mode') != 'custom') {
  671. let random_mode = this._settings.get_string('random-interval-mode');
  672. seconds = Utils.seconds_until(random_mode); // else we shuffle at specified interval (midnight default)
  673. log('shuffle mode = '+random_mode+' = '+seconds+' from now');
  674. }
  675. else {
  676. seconds = this._settings.get_int('random-interval'); // or whatever the user has specified (as a timer)
  677. }
  678. }
  679. this._shuffleTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._selectImage.bind(this, true));
  680. this.shuffledue = GLib.DateTime.new_now_local().add_seconds(seconds);
  681. log('next shuffle in ' + seconds + ' seconds');
  682. }
  683. // auto export Bing data to JSON file if requested
  684. _exportData() {
  685. if (this._settings.get_boolean('always-export-bing-json')) { // save copy of current JSON
  686. Utils.exportBingJSON(this._settings);
  687. }
  688. }
  689. // process Bing metadata
  690. _parseData(data) {
  691. try {
  692. let parsed = JSON.parse(data);
  693. let datamarket = parsed.market.mkt;
  694. let prefmarket = this._settings.get_string('market');
  695. let newImages = Utils.mergeImageLists(this._settings, parsed.images);
  696. if (datamarket != prefmarket && prefmarket != 'auto')
  697. log('WARNING: Bing returning market data for ' + datamarket + ' rather than selected ' + prefmarket);
  698. Utils.purgeImages(this._settings); // delete older images if enabled
  699. //Utils.cleanupImageList(this._settings); // merged into purgeImages
  700. this._downloadAllImages(); // fetch missing images that are still available
  701. Utils.populateImageListResolutions(this._settings);
  702. if (newImages.length > 0 && this._settings.get_boolean('revert-to-current-image')) {
  703. // user wants to switch to the new image when it arrives
  704. this._setStringSetting('selected-image', 'current');
  705. }
  706. if (this._settings.get_boolean('notify')) {
  707. if (!this._settings.get_boolean('notify-only-latest')) {
  708. // notify all new images
  709. newImages.forEach((image) => {
  710. log('New image to notify: ' + Utils.getImageTitle(image));
  711. this._createImageNotification(image);
  712. });
  713. }
  714. else {
  715. // notify only the most recent image
  716. let last = newImages.pop();
  717. if (last) {
  718. log('New image to notify: ' + Utils.getImageTitle(last));
  719. this._createImageNotification(last);
  720. }
  721. }
  722. }
  723. this._restartTimeoutFromLongDate(parsed.images[0].fullstartdate); // timing is set by Bing, and possibly varies by market
  724. this._updatePending = false;
  725. }
  726. catch (error) {
  727. log('_parseData() failed with error ' + error + ' @ '+error.lineNumber);
  728. notifyError('Bing metadata parsing error check ' + error + ' @ '+error.lineNumber);
  729. log(error.stack);
  730. }
  731. }
  732. _cleanUpImages() {
  733. if (this._settings.get_boolean('delete-previous')) {
  734. Utils.purgeImages(this._settings);
  735. }
  736. }
  737. _createImageNotification(image) {
  738. let msg = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.fullstartdate);
  739. let details = Utils.getImageTitle(image);
  740. this._createNotification(msg, details);
  741. log('_createImageNotification: '+msg+' details: '+details);
  742. }
  743. _createNotification(msg, details) {
  744. const systemSource = MessageTray.getSystemSource();
  745. const bingNotify = new MessageTray.Notification({
  746. source: systemSource,
  747. title: msg,
  748. body: details,
  749. gicon: new Gio.ThemedIcon({name: 'image-x-generic'}),
  750. iconName: 'image-x-generic',
  751. });
  752. systemSource.addNotification(bingNotify);
  753. //Main.notify(msg, details);
  754. log('_createNotification: '+msg+' details: '+details);
  755. }
  756. _shuffleImage() {
  757. let image = null;
  758. let imageList = Utils.getImageList(this._settings);
  759. let filter = { 'faves': this._settings.get_boolean('random-mode-include-only-favourites'),
  760. 'hidden': this._settings.get_boolean('random-mode-include-only-unhidden'),
  761. 'min_height': this._settings.get_boolean('random-mode-include-only-uhd')?this._settings.get_int('min-uhd-height'):false
  762. };
  763. let favImageList = Utils.getImageList(this._settings, filter);
  764. if (favImageList.length >= MINIMUM_SHUFFLE_IMAGES) { // we have the minimum images to shuffle, if not fall back to shuffle all iamges
  765. imageList = favImageList;
  766. }
  767. else {
  768. log('not enough filtered images available to shuffle');
  769. }
  770. // shuffle could fail for a number of reasons
  771. try {
  772. this.imageIndex = Utils.getRandomInt(imageList.length);
  773. image = imageList[this.imageIndex];
  774. log('shuffled to image '+image.urlbase);
  775. return image;
  776. }
  777. catch (e) {
  778. log('shuffle failed '+e);
  779. return null;
  780. }
  781. }
  782. _selectImage(force_shuffle = false) {
  783. let imageList = Utils.getImageList(this._settings);
  784. let image = null;
  785. // special values, 'current' is most recent (default mode), 'random' picks one at random, anything else should be filename
  786. if (force_shuffle) {
  787. log('forcing shuffle of image')
  788. image = this._shuffleImage();
  789. if (this._settings.get_boolean('random-mode-enabled'))
  790. this._restartShuffleTimeout();
  791. }
  792. if (!image) {
  793. if (this.selected_image == 'current') {
  794. image = Utils.getCurrentImage(imageList);
  795. this.imageIndex = Utils.getCurrentImageIndex(imageList);
  796. } else {
  797. image = Utils.inImageList(imageList, this.selected_image);
  798. if (!image) // if we didn't find it, try for current
  799. image = Utils.getCurrentImage(imageList);
  800. if (image)
  801. this.imageIndex = Utils.imageIndex(imageList, image.urlbase);
  802. log('_selectImage: ' + this.selected_image + ' = ' + (image && image.urlbase) ? image.urlbase : 'not found');
  803. }
  804. }
  805. if (!image)
  806. return; // could force, image = imageList[0] or perhaps force refresh
  807. if (image.url != '') {
  808. let resolution = Utils.getResolution(this._settings, image);
  809. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  810. // set current image details at extension scope
  811. this.title = image.copyright.replace(/\s*[\(\(].*?[\)\)]\s*/g, '');
  812. this.explanation = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.startdate);
  813. this.copyright = image.copyright.match(/[\(\(]([^)]+)[\)\)]/)[1].replace('\*\*', ''); // Japan locale uses () rather than ()
  814. this.longstartdate = image.fullstartdate;
  815. this.imageinfolink = image.copyrightlink.replace(/^http:\/\//i, 'https://');
  816. this.imageURL = BingURL + image.urlbase + '_' + resolution + '.jpg'+'&qlt=100'; // generate image url for user's resolution @ high quality
  817. this.filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution);
  818. this.dimensions.width = image.width?image.width:null;
  819. this.dimensions.height = image.height?image.height:null;
  820. this.selected_image = Utils.getImageUrlBase(image);
  821. this._setStringSetting('selected-image', this.selected_image);
  822. if (("favourite" in image) && image.favourite === true ) {
  823. this.favourite_status = true;
  824. }
  825. else {
  826. this.favourite_status = false;
  827. }
  828. if (("hidden" in image) && image.hidden === true ) {
  829. this.hidden_status = true;
  830. }
  831. else {
  832. this.hidden_status = false;
  833. }
  834. let file = Gio.file_new_for_path(this.filename);
  835. let file_exists = file.query_exists(null);
  836. let file_info = file_exists ? file.query_info ('*', Gio.FileQueryInfoFlags.NONE, null) : 0;
  837. if (!file_exists || file_info.get_size () == 0) { // file doesn't exist or is empty (probably due to a network error)
  838. this._downloadImage(this.imageURL, file, true);
  839. }
  840. else {
  841. this._setBackground();
  842. this._updatePending = false;
  843. }
  844. }
  845. else {
  846. this.title = _("No wallpaper available");
  847. this.explanation = _("No picture for today.");
  848. this.filename = "";
  849. this._updatePending = false;
  850. }
  851. this._setMenuText();
  852. this._storeState();
  853. }
  854. _imageURL(urlbase, resolution) {
  855. return BingURL + urlbase + '_' + resolution + '.jpg';
  856. }
  857. _storeState() {
  858. if (this.filename) {
  859. let maxLongDate = Utils.getMaxLongDate(this._settings); // refresh date from most recent Bing image
  860. let state = {maxlongdate: maxLongDate, title: this.title, explanation: this.explanation, copyright: this.copyright,
  861. longstartdate: this.longstartdate, imageinfolink: this.imageinfolink, imageURL: this.imageURL,
  862. filename: this.filename, favourite: this.favourite_status, width: this.dimensions.width,
  863. height: this.dimensions.height,
  864. shuffledue: (this.shuffledue.to_unix? this.shuffledue.to_unix():0)
  865. };
  866. let stateJSON = JSON.stringify(state);
  867. log('Storing state as JSON: ' + stateJSON);
  868. this._setStringSetting('state', stateJSON);
  869. }
  870. }
  871. _reStoreState() {
  872. try {
  873. // 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
  874. this._setStringSetting('download-folder', this._settings.get_string('download-folder').replace('$HOME', '~'));
  875. let stateJSON = this._settings.get_string('state');
  876. let state = JSON.parse(stateJSON);
  877. let maxLongDate = null;
  878. log('restoring state...');
  879. maxLongDate = state.maxlongdate ? state.maxlongdate : null;
  880. this.title = state.title;
  881. this.explanation = state.explanation;
  882. this.copyright = state.copyright;
  883. this.longstartdate = state.longstartdate;
  884. this.imageinfolink = state.imageinfolink;
  885. this.imageURL = state.imageURL;
  886. this.filename = state.filename;
  887. this.dimensions.width = state.width;
  888. this.dimensions.height = state.height;
  889. this._selected_image = this._settings.get_string('selected-image');
  890. this.shuffledue = ("shuffledue" in state)? GLib.DateTime.new_from_unix_local(state.shuffledue) : 0;
  891. this.favourite_status = ("favourite" in state && state.favourite === true);
  892. // update menus and thumbnail
  893. this._setMenuText();
  894. this._setBackground();
  895. if (!maxLongDate) {
  896. this._restartTimeout(60);
  897. return;
  898. }
  899. if (this._settings.get_boolean('random-mode-enabled')) {
  900. log('random mode enabled, restarting random state');
  901. this._restartShuffleTimeoutFromDueDate(this.shuffledue); // FIXME: use state value
  902. this._restartTimeoutFromLongDate(maxLongDate);
  903. }
  904. else {
  905. this._restartTimeoutFromLongDate(maxLongDate);
  906. }
  907. return;
  908. }
  909. catch (error) {
  910. log('bad state - refreshing... error was ' + error);
  911. }
  912. this._restartTimeout(60);
  913. }
  914. _downloadAllImages() {
  915. // fetch recent undownloaded images
  916. let imageList = Utils.getFetchableImageList(this._settings);
  917. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  918. imageList.forEach( (image) => {
  919. let resolution = Utils.getResolution(this._settings, image);
  920. let filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution);
  921. let url = this._imageURL(image.urlbase, resolution);
  922. let file = Gio.file_new_for_path(filename);
  923. this._downloadImage(url, file, false);
  924. });
  925. }
  926. // download and process new image
  927. // FIXME: improve error handling
  928. _downloadImage(url, file, set_background) {
  929. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  930. let dir = Gio.file_new_for_path(BingWallpaperDir);
  931. if (!dir.query_exists(null)) {
  932. //dir.make_directory_with_parents(null);
  933. notifyError('Download folder '+BingWallpaperDir+' does not exist or is not writable');
  934. return;
  935. }
  936. log("Downloading " + url + " to " + file.get_uri());
  937. let request = Soup.Message.new('GET', url);
  938. // queue the http request
  939. try {
  940. if (Soup.MAJOR_VERSION >= 3) {
  941. this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => {
  942. this._processFileDownload(message, file, set_background);
  943. });
  944. }
  945. else {
  946. this.httpSession.queue_message(request, (httpSession, message) => {
  947. this._processFileDownload(message, file, set_background);
  948. });
  949. }
  950. }
  951. catch (error) {
  952. log('error sending libsoup message '+error);
  953. notifyError('Network error '+error);
  954. }
  955. }
  956. _processFileDownload(message, file, set_background) {
  957. try {
  958. let data = (Soup.MAJOR_VERSION >= 3) ?
  959. this.httpSession.send_and_read_finish(message).get_data():
  960. message.response_body.flatten().get_as_bytes();
  961. file.replace_contents_bytes_async(
  962. data,
  963. null,
  964. false,
  965. Gio.FileCreateFlags.REPLACE_DESTINATION,
  966. null,
  967. (file, res) => {
  968. try {
  969. file.replace_contents_finish(res);
  970. if (set_background)
  971. this._setBackground();
  972. log('Download successful');
  973. }
  974. catch(e) {
  975. log('Error writing file: ' + e);
  976. notifyError('Image '+file.get_path()+' is not writable, check folder permissions or select a different folder\n'+e);
  977. }
  978. }
  979. );
  980. }
  981. catch (error) {
  982. log('Unable download image '+error);
  983. notifyError('Image '+file.get_path()+' file error, check folder permissions, disk space or select a different folder\n'+e);
  984. }
  985. }
  986. // open image in default image view
  987. _openInSystemViewer() {
  988. Utils.openInSystemViewer(this.filename);
  989. }
  990. // open Bing image information page
  991. _openImageInfoLink() {
  992. if (this.imageinfolink)
  993. Utils.openInSystemViewer(this.imageinfolink, false);
  994. }
  995. stop() {
  996. if (this._timeout)
  997. GLib.source_remove(this._timeout);
  998. if (this._shuffleTimeout)
  999. GLib.source_remove(this._shuffleTimeout);
  1000. this._timeout = undefined;
  1001. this._shuffleTimeout = undefined;
  1002. this.menu.removeAll();
  1003. blur._disable(); // disable blur (blur.js) override and cleanup
  1004. blur = null;
  1005. }
  1006. });
  1007. export default class BingWallpaperExtension extends Extension {
  1008. enable() {
  1009. bingWallpaperIndicator = new BingWallpaperIndicator(this);
  1010. Main.panel.addToStatusArea(IndicatorName, bingWallpaperIndicator);
  1011. }
  1012. disable() {
  1013. bingWallpaperIndicator.stop();
  1014. bingWallpaperIndicator.destroy();
  1015. bingWallpaperIndicator = null;
  1016. }
  1017. }
  1018. function toFilename(wallpaperDir, startdate, imageURL, resolution) {
  1019. return wallpaperDir + startdate + '-' + imageURL.replace(/^.*[\\\/]/, '').replace('th?id=OHR.', '') + '_' + resolution + '.jpg';
  1020. }