extension.js 46 KB

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