extension.js 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. // Bing Wallpaper GNOME extension
  2. // Copyright (C) 2017-2025 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 BingLog(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. BingLog("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. BingLog(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. BingLog(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. BingLog('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. BingLog('key '+key+' set to ' + value + ' (returned ' + (success?'true':'false')+')');
  285. }
  286. _setIntSetting(key, value) {
  287. let success = this._settings.set_int(key, value);
  288. BingLog('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. " (" + (this.refreshdue?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. " (" + (this.refreshdue?Utils.friendly_time_diff(this.shuffledue):"-") + ")";
  320. }
  321. BingLog('refreshduetext :'+this.refreshduetext);
  322. this.refreshDueItem.label.set_text(this.refreshduetext);
  323. }
  324. _setBlur() {
  325. blur._switch(this._settings.get_boolean('override-lockscreen-blur'));
  326. blur.set_blur_strength(this._settings.get_int('lockscreen-blur-strength'));
  327. blur.set_blur_brightness(this._settings.get_int('lockscreen-blur-brightness'));
  328. }
  329. _setImage() {
  330. Utils.validate_imagename(this._settings);
  331. this.selected_image = this._settings.get_string('selected-image');
  332. BingLog('selected image changed to: ' + this.selected_image);
  333. this._selectImage();
  334. //this._setShuffleToggleState();
  335. }
  336. _notifyCurrentImage() {
  337. if (this._settings.get_boolean('notify')) {
  338. let image = this._getCurrentImage();
  339. if (image) {
  340. this._createImageNotification(image);
  341. }
  342. }
  343. }
  344. // set indicator icon (tray icon)
  345. _setIcon() {
  346. Utils.validate_icon(this._settings, this._extension.path);
  347. let icon_name = this._settings.get_string('icon-name');
  348. let gicon = Gio.icon_new_for_string(this._extension.dir.get_child('icons').get_path() + '/' + icon_name + '.svg');
  349. this.icon = new St.Icon({gicon: gicon, style_class: 'system-status-icon'});
  350. BingLog('Replace icon set to: ' + icon_name);
  351. this.remove_all_children();
  352. this.add_child(this.icon);
  353. }
  354. // set backgrounds as requested and set preview image in menu
  355. _setBackground() {
  356. if (this.filename == '')
  357. return;
  358. this.thumbnail = new Thumbnail(this.filename, St.ThemeContext.get_for_stage(global.stage).scale_factor); // use scale factor to make them look nicer
  359. this._setThumbnailImage();
  360. if (!this.dimensions.width || !this.dimensions.height) // if dimensions aren't in image database yet
  361. [this.dimensions.width, this.dimensions.height] = Utils.getFileDimensions(this.filename);
  362. BingLog('image set to : '+this.filename);
  363. if (this._settings.get_boolean('set-background'))
  364. this._setBackgroundDesktop();
  365. }
  366. _setBackgroundDesktop() {
  367. doSetBackground(this.filename, Utils.DESKTOP_SCHEMA);
  368. }
  369. _copyURLToClipboard() {
  370. this.clipboard.setText(this.imageURL);
  371. }
  372. _copyImageToClipboard() {
  373. this.clipboard.setImage(this.filename);
  374. }
  375. // set a timer on when the current image is going to expire
  376. _restartTimeoutFromLongDate(longdate) {
  377. // all Bing times are in UTC (+0)
  378. let refreshDue = Utils.dateFromLongDate(longdate, 86400).to_local();
  379. let now = GLib.DateTime.new_now_local();
  380. let difference = refreshDue.difference(now) / 1000000;
  381. if (difference < 60 || difference > 86400) // clamp to a reasonable range
  382. difference = 60;
  383. difference = difference + 300; // 5 minute fudge offset in case of inaccurate local clock
  384. BingLog('Next refresh due ' + difference + ' seconds from now');
  385. this._restartTimeout(difference);
  386. }
  387. // alternative shuffle mode, not yet enabled
  388. _restartShuffleTimeoutFromDueDate(duedate) {
  389. let now = GLib.DateTime.new_now_local();
  390. let difference = duedate.difference(now) / 1000000;
  391. if (difference < 60 || difference > 86400) // clamp to a reasonable range
  392. difference = 60;
  393. BingLog('Next shuffle due ' + difference + ' seconds from now');
  394. this._restartShuffleTimeout(difference);
  395. }
  396. // convert longdate format into human friendly format
  397. _localeDate(longdate, include_time = false) {
  398. try {
  399. let date = Utils.dateFromLongDate(longdate, 300); // date at update
  400. return date.to_local().format('%Y-%m-%d' + (include_time? ' %X' : '')); // ISO 8601 - https://xkcd.com/1179/
  401. }
  402. catch (e) {
  403. return 'none';
  404. }
  405. }
  406. // set menu text in lieu of a notification/popup
  407. _setMenuText() {
  408. this.titleItem.label.set_text(this.title ? this.title : '');
  409. this.copyrightItem.label.set_text(this.copyright ? this.copyright : '');
  410. this.imageResolutionItem.label.set_text(this.dimensions.width+'px x '+this.dimensions.height+'px');
  411. if (this._settings.get_boolean('show-count-in-image-title') && this.explanation) {
  412. let imageList = JSON.parse(this._settings.get_string('bing-json'));
  413. if (imageList.length > 0)
  414. this.explainItem.label.set_text( this.explanation + ' [' + (this.imageIndex + 1) + '/' + imageList.length + ']');
  415. }
  416. else {
  417. this.explainItem.label.set_text(this.explanation ? this.explanation : '');
  418. }
  419. this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON);
  420. this._setTrashIcon(this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON);
  421. }
  422. _wrapLabelItem(menuItem) {
  423. let clutter_text = menuItem.label.get_clutter_text();
  424. clutter_text.set_line_wrap(true);
  425. clutter_text.set_ellipsize(0);
  426. clutter_text.set_max_length(0);
  427. menuItem.label.set_style('max-width: 420px;');
  428. }
  429. _setControls() {
  430. this.favouriteBtn = this._newMenuIcon(
  431. this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON,
  432. this.controlItem,
  433. this._favouriteImage);
  434. this.trashBtn = this._newMenuIcon(
  435. this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON,
  436. this.controlItem,
  437. this._trashImage);
  438. this.prevBtn = this._newMenuIcon(
  439. ICON_PREVIOUS_BUTTON,
  440. this.controlItem,
  441. this._prevImage);
  442. this.nextBtn = this._newMenuIcon(
  443. ICON_NEXT_BUTTON,
  444. this.controlItem,
  445. this._nextImage);
  446. this.curBtn = this._newMenuIcon(
  447. ICON_CURRENT_BUTTON,
  448. this.controlItem,
  449. this._curImage);
  450. this.randomizeBtn = this._newMenuIcon(
  451. this.ICON_RANDOM,
  452. this.controlItem,
  453. this._selectImage,
  454. null, true);
  455. }
  456. _newMenuIcon(icon_name, parent, fn, position = null, arg = null) {
  457. let gicon = Gio.icon_new_for_string(icon_name);
  458. let icon = new St.Icon({
  459. /*icon_name: icon_name,*/
  460. gicon: gicon,
  461. style_class: 'popup-menu-icon',
  462. x_expand: true,
  463. y_expand: true,
  464. icon_size: this._settings.get_int('controls-icon-size')
  465. });
  466. let iconBtn = new St.Button({
  467. style_class: 'ci-action-btn',
  468. can_focus: true,
  469. child: icon,
  470. /* x_align: Clutter.ActorAlign.END, // FIXME: errors on GNOME 3.28, default to center is ok */
  471. x_expand: true,
  472. y_expand: true
  473. });
  474. if (position !== null) {
  475. parent.insert_child_at_index(iconBtn, position);
  476. }
  477. else {
  478. parent.add_child(iconBtn);
  479. }
  480. iconBtn.connect('button-press-event', fn.bind(this, arg));
  481. return iconBtn;
  482. }
  483. // set menu thumbnail
  484. _setThumbnailImage() {
  485. let pixbuf = this.thumbnail.pixbuf;
  486. let scale_factor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
  487. if (pixbuf == null)
  488. return;
  489. const {width, height} = pixbuf;
  490. if (height == 0) {
  491. return;
  492. }
  493. const [version] = Config.PACKAGE_VERSION.split('.').map(s => Number(s));
  494. const image = new St.ImageContent();
  495. const success = image.set_data.apply(image, [
  496. ...version >= 48 ? [Clutter.get_default_backend().get_cogl_context()] : [],
  497. pixbuf.get_pixels(),
  498. pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888,
  499. width,
  500. height,
  501. pixbuf.get_rowstride(),
  502. ]);
  503. if (!success) {
  504. throw Error("error creating St.ImageContent()");
  505. }
  506. this.thumbnailItem.hexpand = false;
  507. this.thumbnailItem.vexpand = false;
  508. this.thumbnailItem.content = image;
  509. BingLog('scale factor: ' + scale_factor);
  510. this.thumbnailItem.set_size(480*scale_factor, 270*scale_factor);
  511. this.thumbnailItem.setSensitive(true);
  512. }
  513. _nextImage() {
  514. this._gotoImage(1);
  515. }
  516. _prevImage() {
  517. this._gotoImage(-1);
  518. }
  519. _curImage() {
  520. this._setStringSetting('selected-image', 'current');
  521. this._gotoImage(0);
  522. }
  523. _randomModeChanged() {
  524. let randomEnabled = this._settings.get_boolean('random-mode-enabled');
  525. Utils.validate_interval(this._settings);
  526. [this.toggleShuffleOnlyFaves, this.toggleShuffleOnlyUHD /*, this.toggleShuffleOnlyUnhidden*/]
  527. .forEach( x => {
  528. x.setSensitive(randomEnabled);
  529. });
  530. if (randomEnabled) {
  531. BingLog('enabled shuffle mode, by setting a shuffe timer (5 seconds)');
  532. this._restartShuffleTimeout(5);
  533. this._setBooleanSetting('revert-to-current-image', false);
  534. }
  535. else {
  536. // clear shuffle timer
  537. if (this._shuffleTimeout)
  538. GLib.source_remove(this._shuffleTimeout);
  539. this._setBooleanSetting('revert-to-current-image', true);
  540. }
  541. }
  542. _favouriteImage() {
  543. BingLog('favourite image '+this.imageURL+' status was '+this.favourite_status);
  544. this.favourite_status = !this.favourite_status;
  545. Utils.setImageFavouriteStatus(this._settings, this.imageURL, this.favourite_status);
  546. this._setFavouriteIcon(this.favourite_status?this.ICON_FAVE_BUTTON:this.ICON_UNFAVE_BUTTON);
  547. }
  548. _trashImage() {
  549. BingLog('trash image '+this.imageURL+' status was '+this.hidden_status);
  550. this.hidden_status = !this.hidden_status;
  551. Utils.setImageHiddenStatus(this._settings, this.imageURL, this.hidden_status);
  552. this._setTrashIcon(this.hidden_status?this.ICON_UNTRASH_BUTTON:this.ICON_TRASH_BUTTON);
  553. if (this._settings.get_boolean('trash-deletes-images')) {
  554. BingLog('image to be deleted: '+this.filename);
  555. Utils.deleteImage(this.filename);
  556. Utils.validate_imagename(this._settings);
  557. }
  558. }
  559. _setFavouriteIcon(icon_name) {
  560. let gicon = Gio.icon_new_for_string(icon_name);
  561. this.favouriteBtn.get_children().forEach( (x, i) => {
  562. x.set_gicon(gicon);
  563. });
  564. }
  565. _setTrashIcon(icon_name) {
  566. let gicon = Gio.icon_new_for_string(icon_name);
  567. this.trashBtn.get_children().forEach( (x, i) => {
  568. x.set_gicon(gicon);
  569. });
  570. }
  571. _gotoImage(relativePos) {
  572. let imageList = Utils.getImageList(this._settings);
  573. let curIndex = 0;
  574. if (this.selected_image == 'current') {
  575. curIndex = Utils.getCurrentImageIndex(imageList);
  576. }
  577. else {
  578. curIndex = Utils.imageIndex(imageList, this.selected_image);
  579. }
  580. let newImage = Utils.getImageByIndex(imageList, curIndex + relativePos);
  581. if (newImage)
  582. this._setStringSetting('selected-image', newImage.urlbase.replace('/th?id=OHR.', ''));
  583. }
  584. _getCurrentImage() {
  585. let imageList = Utils.getImageList(this._settings);
  586. let curIndex = Utils.getCurrentImageIndex(imageList);
  587. return Utils.getImageByIndex(imageList, curIndex);
  588. }
  589. // download Bing metadata
  590. _refresh() {
  591. if (this._updatePending)
  592. return;
  593. this._updatePending = true;
  594. this._restartTimeout();
  595. let market = this._settings.get_string('market');
  596. // Soup3 should be the version used, but in the past some distros have packaged older versions only
  597. if (Soup.MAJOR_VERSION >= 3) {
  598. let url = BingImageURL;
  599. let params = Utils.BingParams;
  600. params['mkt'] = ( market != 'auto' ? market : '' );
  601. // if we've set previous days to be something less than 8 and
  602. // delete previous is active we want to just request a subset of wallpapers
  603. if (this._settings.get_boolean('delete-previous') == true && this._settings.get_int('previous-days')<8) {
  604. params['n'] = ""+this._settings.get_int('previous-days');
  605. }
  606. let request = Soup.Message.new_from_encoded_form('GET', url, Soup.form_encode_hash(params));
  607. request.request_headers.append('Accept', 'application/json');
  608. try {
  609. this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => {
  610. this._processMessageRefresh(message);
  611. });
  612. }
  613. catch(error) {
  614. BingLog('unable to send libsoup json message '+error);
  615. notifyError('Unable to fetch Bing metadata\n'+error);
  616. }
  617. }
  618. else {
  619. let url = BingImageURL + '?format=js&idx=0&n=8&mbl=1&mkt=' + (market != 'auto' ? market : '');
  620. let request = Soup.Message.new('GET', url);
  621. request.request_headers.append('Accept', 'application/json');
  622. // queue the http request
  623. try {
  624. this.httpSession.queue_message(request, (httpSession, message) => {
  625. this._processMessageRefresh(message);
  626. });
  627. }
  628. catch (error) {
  629. BingLog('unable to send libsoup json message '+error);
  630. notifyError('Unable to fetch Bing metadata\n'+error);
  631. }
  632. }
  633. }
  634. _processMessageRefresh(message) {
  635. const decoder = new TextDecoder();
  636. try {
  637. let data = (Soup.MAJOR_VERSION >= 3) ?
  638. decoder.decode(this.httpSession.send_and_read_finish(message).get_data()): // Soup3
  639. message.response_body.data; // Soup 2
  640. BingLog('Recieved ' + data.length + ' bytes');
  641. this._parseData(data);
  642. if (!this._settings.get_boolean('random-mode-enabled'))
  643. this._selectImage();
  644. }
  645. catch (error) {
  646. BingLog('Network error occured: ' + error);
  647. notifyError('network error occured\n'+error);
  648. this._updatePending = false;
  649. this._restartTimeout(TIMEOUT_SECONDS_ON_HTTP_ERROR);
  650. }
  651. }
  652. // sets a timer for next refresh of Bing metadata
  653. _restartTimeout(seconds = null) {
  654. if (this._timeout)
  655. GLib.source_remove(this._timeout);
  656. if (seconds == null)
  657. seconds = TIMEOUT_SECONDS;
  658. this._timeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._refresh.bind(this));
  659. this.refreshdue = GLib.DateTime.new_now_local().add_seconds(seconds);
  660. BingLog('next check in ' + seconds + ' seconds');
  661. }
  662. _restartShuffleTimeout(seconds = null) {
  663. BingLog('_restartShuffleTimeout('+seconds+')');
  664. //console.trace();
  665. if (this._shuffleTimeout)
  666. GLib.source_remove(this._shuffleTimeout);
  667. if (seconds == null) {
  668. let diff = -Math.floor(GLib.DateTime.new_now_local().difference(this.shuffledue)/1000000);
  669. BingLog('shuffle ('+this.shuffledue.format_iso8601()+') diff = '+diff);
  670. if (diff > 30) { // on occasions the above will be 1 second
  671. seconds = diff; // if not specified, we should maintain the existing shuffle timeout (i.e. we just restored from saved state)
  672. }
  673. else if (this._settings.get_string('random-interval-mode') != 'custom') {
  674. let random_mode = this._settings.get_string('random-interval-mode');
  675. seconds = Utils.seconds_until(random_mode); // else we shuffle at specified interval (midnight default)
  676. BingLog('shuffle mode = '+random_mode+' = '+seconds+' from now');
  677. }
  678. else {
  679. seconds = this._settings.get_int('random-interval'); // or whatever the user has specified (as a timer)
  680. }
  681. }
  682. this._shuffleTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, this._selectImage.bind(this, true));
  683. this.shuffledue = GLib.DateTime.new_now_local().add_seconds(seconds);
  684. BingLog('next shuffle in ' + seconds + ' seconds');
  685. }
  686. // auto export Bing data to JSON file if requested
  687. _exportData() {
  688. if (this._settings.get_boolean('always-export-bing-json')) { // save copy of current JSON
  689. Utils.exportBingJSON(this._settings);
  690. }
  691. }
  692. // process Bing metadata
  693. _parseData(data) {
  694. try {
  695. let parsed = JSON.parse(data);
  696. let datamarket = parsed.market.mkt;
  697. let prefmarket = this._settings.get_string('market');
  698. let newImages = Utils.mergeImageLists(this._settings, parsed.images);
  699. if (datamarket != prefmarket && prefmarket != 'auto')
  700. BingLog('WARNING: Bing returning market data for ' + datamarket + ' rather than selected ' + prefmarket);
  701. Utils.purgeImages(this._settings); // delete older images if enabled
  702. //Utils.cleanupImageList(this._settings); // merged into purgeImages
  703. this._downloadAllImages(); // fetch missing images that are still available
  704. Utils.populateImageListResolutions(this._settings);
  705. if (newImages.length > 0 && this._settings.get_boolean('revert-to-current-image')) {
  706. // user wants to switch to the new image when it arrives
  707. this._setStringSetting('selected-image', 'current');
  708. }
  709. if (this._settings.get_boolean('notify')) {
  710. if (!this._settings.get_boolean('notify-only-latest')) {
  711. // notify all new images
  712. newImages.forEach((image) => {
  713. BingLog('New image to notify: ' + Utils.getImageTitle(image));
  714. this._createImageNotification(image);
  715. });
  716. }
  717. else {
  718. // notify only the most recent image
  719. let last = newImages.pop();
  720. if (last) {
  721. BingLog('New image to notify: ' + Utils.getImageTitle(last));
  722. this._createImageNotification(last);
  723. }
  724. }
  725. }
  726. this._restartTimeoutFromLongDate(parsed.images[0].fullstartdate); // timing is set by Bing, and possibly varies by market
  727. this._updatePending = false;
  728. }
  729. catch (error) {
  730. BingLog('_parseData() failed with error ' + error + ' @ '+error.lineNumber);
  731. notifyError('Bing metadata parsing error check ' + error + ' @ '+error.lineNumber);
  732. BingLog(error.stack);
  733. }
  734. }
  735. _cleanUpImages() {
  736. if (this._settings.get_boolean('delete-previous')) {
  737. Utils.purgeImages(this._settings);
  738. }
  739. }
  740. _createImageNotification(image) {
  741. let msg = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.fullstartdate);
  742. let details = Utils.getImageTitle(image);
  743. this._createNotification(msg, details);
  744. BingLog('_createImageNotification: '+msg+' details: '+details);
  745. }
  746. _createNotification(msg, details) {
  747. const systemSource = MessageTray.getSystemSource();
  748. const bingNotify = new MessageTray.Notification({
  749. source: systemSource,
  750. title: msg,
  751. body: details,
  752. gicon: new Gio.ThemedIcon({name: 'image-x-generic'}),
  753. iconName: 'image-x-generic',
  754. });
  755. systemSource.addNotification(bingNotify);
  756. //Main.notify(msg, details);
  757. BingLog('_createNotification: '+msg+' details: '+details);
  758. }
  759. _shuffleImage() {
  760. let image = null;
  761. let imageList = Utils.getImageList(this._settings);
  762. let filter = { 'faves': this._settings.get_boolean('random-mode-include-only-favourites'),
  763. 'hidden': this._settings.get_boolean('random-mode-include-only-unhidden'),
  764. 'min_height': this._settings.get_boolean('random-mode-include-only-uhd')?this._settings.get_int('min-uhd-height'):false
  765. };
  766. let favImageList = Utils.getImageList(this._settings, filter);
  767. if (favImageList.length >= MINIMUM_SHUFFLE_IMAGES) { // we have the minimum images to shuffle, if not fall back to shuffle all iamges
  768. imageList = favImageList;
  769. }
  770. else {
  771. BingLog('not enough filtered images available to shuffle');
  772. }
  773. // shuffle could fail for a number of reasons
  774. try {
  775. this.imageIndex = Utils.getRandomInt(imageList.length);
  776. image = imageList[this.imageIndex];
  777. BingLog('shuffled to image '+image.urlbase);
  778. return image;
  779. }
  780. catch (e) {
  781. BingLog('shuffle failed '+e);
  782. return null;
  783. }
  784. }
  785. _selectImage(force_shuffle = false) {
  786. let imageList = Utils.getImageList(this._settings);
  787. let image = null;
  788. // special values, 'current' is most recent (default mode), 'random' picks one at random, anything else should be filename
  789. if (force_shuffle) {
  790. BingLog('forcing shuffle of image')
  791. image = this._shuffleImage();
  792. if (this._settings.get_boolean('random-mode-enabled'))
  793. this._restartShuffleTimeout();
  794. }
  795. if (!image) {
  796. if (this.selected_image == 'current') {
  797. image = Utils.getCurrentImage(imageList);
  798. this.imageIndex = Utils.getCurrentImageIndex(imageList);
  799. } else {
  800. image = Utils.inImageList(imageList, this.selected_image);
  801. if (!image) // if we didn't find it, try for current
  802. image = Utils.getCurrentImage(imageList);
  803. if (image)
  804. this.imageIndex = Utils.imageIndex(imageList, image.urlbase);
  805. BingLog('_selectImage: ' + this.selected_image + ' = ' + (image && image.urlbase ? image.urlbase : 'not found'));
  806. }
  807. }
  808. if (!image)
  809. return; // could force, image = imageList[0] or perhaps force refresh
  810. if (image.url != '') {
  811. let resolution = Utils.getResolution(this._settings, image);
  812. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  813. // set current image details at extension scope
  814. this.title = image.copyright.replace(/\s*[\(\(].*?[\)\)]\s*/g, '');
  815. this.explanation = _('Bing Wallpaper of the Day for') + ' ' + this._localeDate(image.startdate);
  816. this.copyright = image.copyright.match(/[\(\(]([^)]+)[\)\)]/)[1].replace('\*\*', ''); // Japan locale uses () rather than ()
  817. this.longstartdate = image.fullstartdate;
  818. this.imageinfolink = image.copyrightlink.replace(/^http:\/\//i, 'https://');
  819. this.imageURL = BingURL + image.urlbase + '_' + resolution + '.jpg'+'&qlt=100'; // generate image url for user's resolution @ high quality
  820. this.filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution);
  821. this.dimensions.width = image.width?image.width:null;
  822. this.dimensions.height = image.height?image.height:null;
  823. this.selected_image = Utils.getImageUrlBase(image);
  824. this._setStringSetting('selected-image', this.selected_image);
  825. if (("favourite" in image) && image.favourite === true ) {
  826. this.favourite_status = true;
  827. }
  828. else {
  829. this.favourite_status = false;
  830. }
  831. if (("hidden" in image) && image.hidden === true ) {
  832. this.hidden_status = true;
  833. }
  834. else {
  835. this.hidden_status = false;
  836. }
  837. let file = Gio.file_new_for_path(this.filename);
  838. let file_exists = file.query_exists(null);
  839. let file_info = file_exists ? file.query_info ('*', Gio.FileQueryInfoFlags.NONE, null) : 0;
  840. if (!file_exists || file_info.get_size () == 0) { // file doesn't exist or is empty (probably due to a network error)
  841. this._downloadImage(this.imageURL, file, true);
  842. }
  843. else {
  844. this._setBackground();
  845. this._updatePending = false;
  846. }
  847. }
  848. else {
  849. this.title = _("No wallpaper available");
  850. this.explanation = _("No picture for today.");
  851. this.filename = "";
  852. this._updatePending = false;
  853. }
  854. this._setMenuText();
  855. this._storeState();
  856. }
  857. _imageURL(urlbase, resolution) {
  858. return BingURL + urlbase + '_' + resolution + '.jpg';
  859. }
  860. _storeState() {
  861. if (this.filename) {
  862. let maxLongDate = Utils.getMaxLongDate(this._settings); // refresh date from most recent Bing image
  863. let state = {maxlongdate: maxLongDate, title: this.title, explanation: this.explanation, copyright: this.copyright,
  864. longstartdate: this.longstartdate, imageinfolink: this.imageinfolink, imageURL: this.imageURL,
  865. filename: this.filename, favourite: this.favourite_status, width: this.dimensions.width,
  866. height: this.dimensions.height,
  867. shuffledue: (this.shuffledue.to_unix? this.shuffledue.to_unix():0)
  868. };
  869. let stateJSON = JSON.stringify(state);
  870. BingLog('Storing state as JSON: ' + stateJSON);
  871. this._setStringSetting('state', stateJSON);
  872. }
  873. }
  874. _reStoreState() {
  875. try {
  876. // 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
  877. this._setStringSetting('download-folder', this._settings.get_string('download-folder').replace('$HOME', '~'));
  878. let stateJSON = this._settings.get_string('state');
  879. let state = JSON.parse(stateJSON);
  880. let maxLongDate = null;
  881. BingLog('restoring state...');
  882. maxLongDate = state.maxlongdate ? state.maxlongdate : null;
  883. this.title = state.title;
  884. this.explanation = state.explanation;
  885. this.copyright = state.copyright;
  886. this.longstartdate = state.longstartdate;
  887. this.imageinfolink = state.imageinfolink;
  888. this.imageURL = state.imageURL;
  889. this.filename = state.filename;
  890. this.dimensions.width = state.width;
  891. this.dimensions.height = state.height;
  892. this._selected_image = this._settings.get_string('selected-image');
  893. this.shuffledue = ("shuffledue" in state)? GLib.DateTime.new_from_unix_local(state.shuffledue) : 0;
  894. this.favourite_status = ("favourite" in state && state.favourite === true);
  895. // update menus and thumbnail
  896. this._setMenuText();
  897. this._setBackground();
  898. if (!maxLongDate) {
  899. this._restartTimeout(60);
  900. return;
  901. }
  902. if (this._settings.get_boolean('random-mode-enabled')) {
  903. BingLog('random mode enabled, restarting random state');
  904. this._restartShuffleTimeoutFromDueDate(this.shuffledue); // FIXME: use state value
  905. this._restartTimeoutFromLongDate(maxLongDate);
  906. }
  907. else {
  908. this._restartTimeoutFromLongDate(maxLongDate);
  909. }
  910. return;
  911. }
  912. catch (error) {
  913. BingLog('bad state - refreshing... error was ' + error);
  914. }
  915. this._restartTimeout(60);
  916. }
  917. _downloadAllImages() {
  918. // fetch recent undownloaded images
  919. let imageList = Utils.getFetchableImageList(this._settings);
  920. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  921. imageList.forEach( (image) => {
  922. let resolution = Utils.getResolution(this._settings, image);
  923. let filename = Utils.toFilename(BingWallpaperDir, image.startdate, image.urlbase, resolution);
  924. let url = this._imageURL(image.urlbase, resolution);
  925. let file = Gio.file_new_for_path(filename);
  926. this._downloadImage(url, file, false);
  927. });
  928. }
  929. // download and process new image
  930. // FIXME: improve error handling
  931. _downloadImage(url, file, set_background) {
  932. let BingWallpaperDir = Utils.getWallpaperDir(this._settings);
  933. let dir = Gio.file_new_for_path(BingWallpaperDir);
  934. if (!dir.query_exists(null)) {
  935. //dir.make_directory_with_parents(null);
  936. notifyError('Download folder '+BingWallpaperDir+' does not exist or is not writable');
  937. return;
  938. }
  939. BingLog("Downloading " + url + " to " + file.get_uri());
  940. let request = Soup.Message.new('GET', url);
  941. // queue the http request
  942. try {
  943. if (Soup.MAJOR_VERSION >= 3) {
  944. this.httpSession.send_and_read_async(request, GLib.PRIORITY_DEFAULT, null, (httpSession, message) => {
  945. this._processFileDownload(message, file, set_background);
  946. });
  947. }
  948. else {
  949. this.httpSession.queue_message(request, (httpSession, message) => {
  950. this._processFileDownload(message, file, set_background);
  951. });
  952. }
  953. }
  954. catch (error) {
  955. BingLog('error sending libsoup message '+error);
  956. notifyError('Network error '+error);
  957. }
  958. }
  959. _processFileDownload(message, file, set_background) {
  960. try {
  961. let data = (Soup.MAJOR_VERSION >= 3) ?
  962. this.httpSession.send_and_read_finish(message).get_data():
  963. message.response_body.flatten().get_as_bytes();
  964. file.replace_contents_bytes_async(
  965. data,
  966. null,
  967. false,
  968. Gio.FileCreateFlags.REPLACE_DESTINATION,
  969. null,
  970. (file, res) => {
  971. try {
  972. file.replace_contents_finish(res);
  973. if (set_background)
  974. this._setBackground();
  975. BingLog('Download successful');
  976. }
  977. catch(e) {
  978. BingLog('Error writing file: ' + e);
  979. notifyError('Image '+file.get_path()+' is not writable, check folder permissions or select a different folder\n'+e);
  980. }
  981. }
  982. );
  983. }
  984. catch (error) {
  985. BingLog('Unable download image '+error);
  986. notifyError('Image '+file.get_path()+' file error, check folder permissions, disk space or select a different folder\n'+e);
  987. }
  988. }
  989. // open image in default image view
  990. _openInSystemViewer() {
  991. Utils.openInSystemViewer(this.filename);
  992. }
  993. // open Bing image information page
  994. _openImageInfoLink() {
  995. if (this.imageinfolink)
  996. Utils.openInSystemViewer(this.imageinfolink, false);
  997. }
  998. stop() {
  999. if (this._timeout)
  1000. GLib.source_remove(this._timeout);
  1001. if (this._shuffleTimeout)
  1002. GLib.source_remove(this._shuffleTimeout);
  1003. this._timeout = undefined;
  1004. this._shuffleTimeout = undefined;
  1005. this.menu.removeAll();
  1006. blur._disable(); // disable blur (blur.js) override and cleanup
  1007. blur = null;
  1008. }
  1009. });
  1010. export default class BingWallpaperExtension extends Extension {
  1011. enable() {
  1012. bingWallpaperIndicator = new BingWallpaperIndicator(this);
  1013. Main.panel.addToStatusArea(IndicatorName, bingWallpaperIndicator);
  1014. }
  1015. disable() {
  1016. bingWallpaperIndicator.stop();
  1017. bingWallpaperIndicator.destroy();
  1018. bingWallpaperIndicator = null;
  1019. }
  1020. }
  1021. function toFilename(wallpaperDir, startdate, imageURL, resolution) {
  1022. return wallpaperDir + startdate + '-' + imageURL.replace(/^.*[\\\/]/, '').replace('th?id=OHR.', '') + '_' + resolution + '.jpg';
  1023. }