diff --git a/README.md b/README.md index d1ee7269..408e0499 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ Requires [`blueprint-compiler`](https://jwestman.pages.gitlab.gnome.org/blueprin Run `./build.sh` to compile ui files. ## Adding predefined sources -1. Build UI for settings using the [blueprint-compiler](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) language in `…/ui/my_source.blp` - see [Workbench](https://apps.gnome.org/app/re.sonny.Workbench/) for a live preview editor. +1. Build UI for settings using the [blueprint-compiler](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) language in `…/ui/mySource.blp` - see [Workbench](https://apps.gnome.org/app/re.sonny.Workbench/) for a live preview editor. * Add the file to `build.sh` 1. Create a settings layout to the `…/schemas/….gschema.xml` -1. Create your logic hooking the settings in a `…/ui/my_source.js` -1. Add the new source to `…/ui/source_row.js` - * As string for the the ComboRow in `…/ui/source_row.blp` -1. Create a adapter to read the settings and fetching the images and additional information in `…/sourceAdapter.js` +1. Create your logic hooking the settings in a `…/ui/mySource.js` +1. Add the new source to `…/ui/sourceRow.js` + * As string for the the ComboRow in `…/ui/sourceRow.blp` +1. Create a adapter to read the settings and fetching the images and additional information in `…/adapter/myAdapter.js` by extending the `BaseAdapter`. * Add your adapter to `…/wallpaperController.js` ## Support Me diff --git a/build.sh b/build.sh index d32703d8..eb966cce 100755 --- a/build.sh +++ b/build.sh @@ -10,11 +10,12 @@ glib-compile-schemas "$BASEDIR/schemas/" # cd "$BASEDIR/ui" || exit 1 blueprint-compiler batch-compile "$BASEDIR/ui" "$BASEDIR/ui" \ - "$BASEDIR/ui/generic_json.blp" \ - "$BASEDIR/ui/page_general.blp" \ - "$BASEDIR/ui/page_sources.blp" \ + "$BASEDIR/ui/genericJson.blp" \ + "$BASEDIR/ui/localFolder.blp" \ + "$BASEDIR/ui/pageGeneral.blp" \ + "$BASEDIR/ui/pageSources.blp" \ "$BASEDIR/ui/reddit.blp" \ - "$BASEDIR/ui/source_row.blp" \ + "$BASEDIR/ui/sourceRow.blp" \ "$BASEDIR/ui/unsplash.blp" \ "$BASEDIR/ui/wallhaven.blp" diff --git a/randomwallpaper@iflow.space/adapter/baseAdapter.js b/randomwallpaper@iflow.space/adapter/baseAdapter.js new file mode 100755 index 00000000..388c1ba3 --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/baseAdapter.js @@ -0,0 +1,110 @@ +const Gio = imports.gi.Gio; + +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const LoggerModule = Self.imports.logger; + +/* + libSoup is accessed through the SoupBowl wrapper to support libSoup3 and libSoup2.4 simultaneously in the extension + runtime and in the preferences window. + */ +const SoupBowl = Self.imports.soupBowl; + +var BaseAdapter = class { + _wallpaperLocation = null; + + constructor(wallpaperLocation) { + this.logger = new LoggerModule.Logger('RWG3', 'BaseAdapter'); + + this._wallpaperLocation = wallpaperLocation; + } + + /** + * Retrieves a new url for an image and calls the given callback with an HistoryEntry as parameter. + * The history element will be null and the error will be set if an error occurred. + * + * @param callback(historyElement, error) + */ + requestRandomImage(callback) { + this._error("requestRandomImage not implemented", callback); + } + + fileName(uri) { + while (this._isURIEncoded(uri)) { + uri = decodeURIComponent(uri); + } + + let base = uri.substring(uri.lastIndexOf('/') + 1); + if (base.indexOf('?') >= 0) { + base = base.substr(0, base.indexOf('?')); + } + return base; + } + + /** + * copy file from uri to local wallpaper directory and calls the given callback with the name and the full filepath + * of the written file as parameter. + * @param uri + * @param callback(name, path, error) + */ + fetchFile(uri, callback) { + //extract the name from the url and + let date = new Date(); + let name = date.getTime() + '_' + this.fileName(uri); // timestamp ensures uniqueness + + let bowl = new SoupBowl.Bowl(); + + let file = Gio.file_new_for_path(this._wallpaperLocation + String(name)); + let fstream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); + + // start the download + let request = bowl.Soup.Message.new('GET', uri); + + bowl.send_and_receive(request, (response_data_bytes) => { + if (!response_data_bytes) { + fstream.close(null); + + if (callback) { + callback(null, null, 'Not a valid response'); + } + + return; + } + + try { + fstream.write(response_data_bytes, null); + + fstream.close(null); + + // call callback with the name and the full filepath of the written file as parameter + if (callback) { + callback(name, file.get_path()); + } + } catch (e) { + if (callback) { + callback(null, null, e); + } + } + }); + } + + _isURIEncoded(uri) { + uri = uri || ''; + + try { + return uri !== decodeURIComponent(uri); + } catch (err) { + this.logger.error(err); + return false; + } + } + + _error(err, callback) { + let error = { "error": err }; + this.logger.error(JSON.stringify(error)); + + if (callback) { + callback(null, error); + } + } + +}; diff --git a/randomwallpaper@iflow.space/adapter/genericJson.js b/randomwallpaper@iflow.space/adapter/genericJson.js new file mode 100644 index 00000000..0e8b5034 --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/genericJson.js @@ -0,0 +1,105 @@ +const ByteArray = imports.byteArray; + +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const HistoryModule = Self.imports.history; +const JSONPath = Self.imports.jsonpath.jsonpath; +const SettingsModule = Self.imports.settings; +const SoupBowl = Self.imports.soupBowl; + +const BaseAdapter = Self.imports.adapter.baseAdapter; + +const RWG_SETTINGS_SCHEMA_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; + +var GenericJsonAdapter = class extends BaseAdapter.BaseAdapter { + constructor(id, wallpaperLocation) { + super(wallpaperLocation); + this._jsonPathParser = new JSONPath.JSONPathParser(); + let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/genericJSON/${id}/`; + this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_GENERIC_JSON, path); + this.bowl = new SoupBowl.Bowl(); + } + + requestRandomImage(callback) { + let url = this._settings.get("request-url", "string"); + url = encodeURI(url); + let message = this.bowl.Soup.Message.new('GET', url); + if (message === null) { + this._error("Could not create request.", callback); + return; + } + + this.bowl.send_and_receive(message, (response_body_bytes) => { + try { + const response_body = JSON.parse(ByteArray.toString(response_body_bytes)); + + let imageJSONPath = this._settings.get("image-path", "string"); + let postJSONPath = this._settings.get("post-path", "string"); + let domainUrl = this._settings.get("domain", "string"); + let authorNameJSONPath = this._settings.get("author-name-path", "string"); + let authorUrlJSONPath = this._settings.get("author-url-path", "string"); + + let identifier = this._settings.get("name", "string"); + if (identifier === null || identifier === "") { + identifier = 'Generic JSON Source'; + } + + let rObject = this._jsonPathParser.access(response_body, imageJSONPath); + let imageDownloadUrl = this._settings.get("image-prefix", "string") + rObject.Object; + + // '@random' would yield different results so lets make sure the values stay + // the same as long as the path is identical + let samePath = imageJSONPath.substring(0, this.findFirstDifference(imageJSONPath, postJSONPath)); + + // count occurrences of '@random' to slice the array later + // https://stackoverflow.com/a/4009768 + let occurrences = (samePath.match(/@random/g) || []).length; + let slicedRandomElements = rObject.RandomElements.slice(0, occurrences); + + let postUrl = this._jsonPathParser.access(response_body, postJSONPath, slicedRandomElements, false).Object; + postUrl = this._settings.get("post-prefix", "string") + postUrl; + if (typeof postUrl !== 'string' || !postUrl instanceof String) { + postUrl = null; + } + + let authorName = this._jsonPathParser.access(response_body, authorNameJSONPath, slicedRandomElements, false).Object; + if (typeof authorName !== 'string' || !authorName instanceof String) { + authorName = null; + } + + let authorUrl = this._jsonPathParser.access(response_body, authorUrlJSONPath, slicedRandomElements, false).Object; + authorUrl = this._settings.get("author-url-prefix", "string") + authorUrl; + if (typeof authorUrl !== 'string' || !authorUrl instanceof String) { + authorUrl = null; + } + + if (callback) { + let historyEntry = new HistoryModule.HistoryEntry(authorName, identifier, imageDownloadUrl); + + if (authorUrl !== null && authorUrl !== "") { + historyEntry.source.authorUrl = authorUrl; + } + + if (postUrl !== null && postUrl !== "") { + historyEntry.source.imageLinkUrl = postUrl; + } + + if (domainUrl !== null && domainUrl !== "") { + historyEntry.source.sourceUrl = domainUrl; + } + + callback(historyEntry); + } + } catch (e) { + this._error("Unexpected response. (" + e + ")", callback); + } + }); + } + + // https://stackoverflow.com/a/32859917 + findFirstDifference(jsonPath1, jsonPath2) { + let i = 0; + if (jsonPath1 === jsonPath2) return -1; + while (jsonPath1[i] === jsonPath2[i]) i++; + return i; + } +}; diff --git a/randomwallpaper@iflow.space/adapter/localFolder.js b/randomwallpaper@iflow.space/adapter/localFolder.js new file mode 100644 index 00000000..1a45bf44 --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/localFolder.js @@ -0,0 +1,87 @@ +const Gio = imports.gi.Gio; + +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const SettingsModule = Self.imports.settings; +const HistoryModule = Self.imports.history; + +const BaseAdapter = Self.imports.adapter.baseAdapter; + +const RWG_SETTINGS_SCHEMA_LOCAL_FOLDER = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.localFolder'; + +var LocalFolderAdapter = class extends BaseAdapter.BaseAdapter { + constructor(id, wallpaperLocation) { + super(wallpaperLocation); + + let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/localFolder/${id}/`; + this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_LOCAL_FOLDER, path); + } + + requestRandomImage(callback) { + const folder = Gio.file_new_for_path(this._settings.get('folder', 'string')); + let files = this._listDirectory(folder); + + if (files === null || files.length < 1) { + this._error("Empty array.", callback); + } + + let randomFile = files[Math.floor(Math.random() * files.length)].get_path(); + + let identifier = this._settings.get("name", "string"); + if (identifier === null || identifier === "") { + identifier = 'Local Folder'; + } + + if (callback) { + let historyEntry = new HistoryModule.HistoryEntry(null, identifier, randomFile); + historyEntry.source.sourceUrl = this._wallpaperLocation; + callback(historyEntry); + } + } + + fetchFile(path, callback) { + let date = new Date(); + let sourceFile = Gio.file_new_for_path(path); + let name = `${date.getTime()}_${sourceFile.get_basename()}` + let targetFile = Gio.file_new_for_path(this._wallpaperLocation + String(name)); + + // https://gjs.guide/guides/gio/file-operations.html#copying-and-moving-files + sourceFile.copy(targetFile, Gio.FileCopyFlags.NONE, null, null); + + if (callback) { + callback(name, targetFile.get_path()); + } + } + + // https://gjs.guide/guides/gio/file-operations.html#recursively-deleting-a-directory + _listDirectory(directory) { + const iterator = directory.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); + + let files = []; + while (true) { + const info = iterator.next_file(null); + + if (info === null) { + break; + } + + const child = iterator.get_child(info); + const type = info.get_file_type(); + + switch (type) { + case Gio.FileType.DIRECTORY: + files = files.concat(this._listDirectory(child)); + break; + + default: + break; + } + + let contentType = info.get_content_type(); + if (contentType === 'image/png' || contentType === 'image/jpeg') { + files.push(child); + } + } + + return files; + } +}; diff --git a/randomwallpaper@iflow.space/adapter/reddit.js b/randomwallpaper@iflow.space/adapter/reddit.js new file mode 100644 index 00000000..cc8033f4 --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/reddit.js @@ -0,0 +1,68 @@ +const ByteArray = imports.byteArray; + +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const SettingsModule = Self.imports.settings; +const HistoryModule = Self.imports.history; +const SoupBowl = Self.imports.soupBowl; + +const BaseAdapter = Self.imports.adapter.baseAdapter; + +const RWG_SETTINGS_SCHEMA_REDDIT = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.reddit'; + +var RedditAdapter = class extends BaseAdapter.BaseAdapter { + constructor(id, wallpaperLocation) { + super(wallpaperLocation); + + let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/reddit/${id}/`; + this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_REDDIT, path); + this.bowl = new SoupBowl.Bowl(); + } + + _ampDecode(string) { + return string.replace(/\&/g, '&'); + } + + requestRandomImage(callback) { + const subreddits = this._settings.get('subreddits', 'string').split(',').map(s => s.trim()).join('+'); + const require_sfw = this._settings.get('allow-sfw', 'boolean'); + let identifier = this._settings.get("name", "string"); + if (identifier === null || identifier === "") { + identifier = 'Reddit'; + } + + const url = encodeURI('https://www.reddit.com/r/' + subreddits + '.json'); + let message = this.bowl.Soup.Message.new('GET', url); + if (message === null) { + this._error("Could not create request.", callback); + return; + } + + this.bowl.send_and_receive(message, (response_body_bytes) => { + try { + const response_body = JSON.parse(ByteArray.toString(response_body_bytes)); + + const submissions = response_body.data.children.filter(child => { + if (child.data.post_hint !== 'image') return false; + if (require_sfw) return child.data.over_18 === false; + return true; + }); + if (submissions.length === 0) { + this._error("No suitable submissions found!", callback); + return; + } + const random = Math.floor(Math.random() * submissions.length); + const submission = submissions[random].data; + const imageDownloadUrl = this._ampDecode(submission.preview.images[0].source.url); + + if (callback) { + let historyEntry = new HistoryModule.HistoryEntry(null, identifier, imageDownloadUrl); + historyEntry.source.sourceUrl = 'https://www.reddit.com/' + submission.subreddit_name_prefixed; + historyEntry.source.imageLinkUrl = 'https://www.reddit.com/' + submission.permalink; + callback(historyEntry); + } + } catch (e) { + this._error("Could not create request. (" + e + ")", callback); + } + }); + } +}; diff --git a/randomwallpaper@iflow.space/adapter/unsplash.js b/randomwallpaper@iflow.space/adapter/unsplash.js new file mode 100644 index 00000000..e984bdec --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/unsplash.js @@ -0,0 +1,124 @@ +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const SettingsModule = Self.imports.settings; +const HistoryModule = Self.imports.history; +const SoupBowl = Self.imports.soupBowl; + +const BaseAdapter = Self.imports.adapter.baseAdapter; + +const RWG_SETTINGS_SCHEMA_UNSPLASH = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.unsplash'; + +var UnsplashAdapter = class extends BaseAdapter.BaseAdapter { + constructor(id, wallpaperLocation) { + super(wallpaperLocation); + + this.sourceName = 'Unsplash'; + this.sourceUrl = 'https://source.unsplash.com'; + + // query options + this.options = { + 'query': '', + 'w': 1920, + 'h': 1080, + 'featured': false, + 'constraintType': '', + 'constraintValue': '', + }; + + if (id === null) { + id = -1; + } + + let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/unsplash/${id}/`; + this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_UNSPLASH, path); + this.bowl = new SoupBowl.Bowl(); + } + + requestRandomImage(callback) { + this._readOptionsFromSettings(); + let optionsString = this._generateOptionsString(); + + let identifier = this._settings.get("name", "string"); + if (identifier === null || identifier === "") { + identifier = this.sourceName; + } + + let url = `https://source.unsplash.com${optionsString}`; + url = encodeURI(url); + + this.logger.info(`Unsplash request to: ${url}`); + let message = this.bowl.Soup.Message.new('GET', url); + if (message === null) { + this._error("Could not create request.", callback); + return; + } + + // unsplash redirects to actual file; we only want the file location + message.set_flags(this.bowl.Soup.MessageFlags.NO_REDIRECT); + + this.bowl.send_and_receive(message, (_null_expected) => { + let imageLinkUrl; + + // expecting redirect + if (message.status_code !== 302) { + this._error("Unexpected response status code (expected 302)", callback); + } + + imageLinkUrl = message.response_headers.get_one('Location'); + + let historyEntry = new HistoryModule.HistoryEntry(null, identifier, imageLinkUrl); + historyEntry.source.sourceUrl = this.sourceUrl; + historyEntry.source.imageLinkUrl = imageLinkUrl; + callback(historyEntry); + }); + } + + _generateOptionsString() { + let options = this.options; + let optionsString = ""; + + switch (options.constraintType) { + case 'user': + optionsString = `/user/${options.constraintValue}/`; + break; + case 'likes': + optionsString = `/user/${options.constraintValue}/likes/`; + break; + case 'collection': + optionsString = `/collection/${options.constraintValue}/`; + break; + default: + if (options.featured) { + optionsString = `/featured/`; + } else { + optionsString = `/random/`; + } + } + + if (options.w && options.h) { + optionsString += `${options.w}x${options.h}`; + } + + if (options.query) { + let q = options.query.replace(/\W/, ','); + optionsString += `?${q}`; + } + + return optionsString; + } + + _readOptionsFromSettings() { + this.options.w = this._settings.get('image-width', 'int'); + this.options.h = this._settings.get('image-height', 'int'); + + this.options.constraintType = this._settings.get('constraint-type', 'string'); + this.options.constraintValue = this._settings.get('constraint-value', 'string'); + + const keywords = this._settings.get('keyword', 'string').split(","); + if (keywords.length > 0) { + const randomKeyword = keywords[Math.floor(Math.random() * keywords.length)]; + this.options.query = randomKeyword.trim(); + } + + this.options.featured = this._settings.get('featured-only', 'boolean'); + } +}; diff --git a/randomwallpaper@iflow.space/adapter/wallhaven.js b/randomwallpaper@iflow.space/adapter/wallhaven.js new file mode 100644 index 00000000..870b7316 --- /dev/null +++ b/randomwallpaper@iflow.space/adapter/wallhaven.js @@ -0,0 +1,119 @@ +const ByteArray = imports.byteArray; + +const Self = imports.misc.extensionUtils.getCurrentExtension(); +const SettingsModule = Self.imports.settings; +const HistoryModule = Self.imports.history; +const SoupBowl = Self.imports.soupBowl; + +const BaseAdapter = Self.imports.adapter.baseAdapter; + +const RWG_SETTINGS_SCHEMA_WALLHAVEN = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.wallhaven'; + +var WallhavenAdapter = class extends BaseAdapter.BaseAdapter { + constructor(id, wallpaperLocation) { + super(wallpaperLocation); + + this.options = { + 'q': '', + 'apikey': '', + 'purity': '110', // SFW, sketchy + 'sorting': 'random', + 'categories': '111', // General, Anime, People + 'resolutions': ['1920x1200', '2560x1440'] + }; + + let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/wallhaven/${id}/`; + this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_WALLHAVEN, path); + this.bowl = new SoupBowl.Bowl(); + } + + requestRandomImage(callback) { + this._readOptionsFromSettings(); + let optionsString = this._generateOptionsString(); + + let identifier = this._settings.get("name", "string"); + if (identifier === null || identifier === "") { + identifier = 'Wallhaven'; + } + + let url = 'https://wallhaven.cc/api/v1/search?' + encodeURI(optionsString); + let message = this.bowl.Soup.Message.new('GET', url); + if (message === null) { + this._error("Could not create request.", callback); + return; + } + + this.bowl.send_and_receive(message, (response_body_bytes) => { + const response_body = ByteArray.toString(response_body_bytes); + + let response = JSON.parse(response_body).data; + + if (!response || response.length === 0) { + this._error("Failed to request image.", callback); + return; + } + + // get a random entry from the array + let entry = response[Math.floor(Math.random() * response.length)]; + let downloadURL = entry.path; + let siteURL = entry.url; + + let apiKey = this.options["apikey"]; + if (apiKey) { + downloadURL += "?apikey=" + apiKey; + } + + if (callback) { + let historyEntry = new HistoryModule.HistoryEntry(null, identifier, downloadURL); + historyEntry.source.sourceUrl = 'https://wallhaven.cc/'; + historyEntry.source.imageLinkUrl = siteURL; + callback(historyEntry); + } + }); + } + + _generateOptionsString() { + let options = this.options; + let optionsString = ""; + + for (let key in options) { + if (options.hasOwnProperty(key)) { + if (Array.isArray(options[key])) { + optionsString += key + "=" + options[key].join() + "&"; + } else { + if (options[key]) { + optionsString += key + "=" + options[key] + "&"; + } + } + } + } + + return optionsString; + } + + _readOptionsFromSettings() { + const keywords = this._settings.get('keyword', 'string').split(","); + if (keywords.length > 0) { + const randomKeyword = keywords[Math.floor(Math.random() * keywords.length)]; + this.options.q = randomKeyword.trim(); + } + this.options.apikey = this._settings.get('api-key', 'string'); + + this.options.resolutions = this._settings.get('resolutions', 'string').split(','); + this.options.resolutions = this.options.resolutions.map((elem) => { + return elem.trim(); + }); + + let categories = []; + categories.push(+this._settings.get('category-general', 'boolean')); // + is implicit conversion to int + categories.push(+this._settings.get('category-anime', 'boolean')); + categories.push(+this._settings.get('category-people', 'boolean')); + this.options.categories = categories.join(''); + + let purity = []; + purity.push(+this._settings.get('allow-sfw', 'boolean')); + purity.push(+this._settings.get('allow-sketchy', 'boolean')); + purity.push(+this._settings.get('allow-nsfw', 'boolean')); + this.options.purity = purity.join(''); + } +}; diff --git a/randomwallpaper@iflow.space/prefs.js b/randomwallpaper@iflow.space/prefs.js index 8e85b129..dbe05fc6 100644 --- a/randomwallpaper@iflow.space/prefs.js +++ b/randomwallpaper@iflow.space/prefs.js @@ -4,7 +4,7 @@ const Gtk = imports.gi.Gtk; const ExtensionUtils = imports.misc.extensionUtils; const Self = ExtensionUtils.getCurrentExtension(); -const SourceRow = Self.imports.ui.source_row; +const SourceRow = Self.imports.ui.sourceRow; const Settings = Self.imports.settings; const WallpaperController = Self.imports.wallpaperController; const LoggerModule = Self.imports.logger; @@ -52,8 +52,8 @@ var RandomWallpaperSettings = class { this._builder = new Gtk.Builder(); //this._builder.set_translation_domain(Self.metadata['gettext-domain']); - this._builder.add_from_file(Self.path + '/ui/page_general.ui'); - this._builder.add_from_file(Self.path + '/ui/page_sources.ui'); + this._builder.add_from_file(Self.path + '/ui/pageGeneral.ui'); + this._builder.add_from_file(Self.path + '/ui/pageSources.ui'); this._loadSources(); @@ -105,7 +105,7 @@ var RandomWallpaperSettings = class { this._builder.get_object('sources_list').add(new_row); this.available_rows[new_row.id] = new_row; - this._bind_source_row(new_row); + this._bindSourceRow(new_row); }); } @@ -141,11 +141,11 @@ var RandomWallpaperSettings = class { this.available_rows[source_row.id] = source_row; this._builder.get_object('sources_list').add(source_row); - this._bind_source_row(source_row); + this._bindSourceRow(source_row); }); } - _bind_source_row(source_row) { + _bindSourceRow(source_row) { source_row.connect('notify::expanded', (row) => { if (!row.expanded) { this._saveSources(); diff --git a/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml b/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml index 5a8d96de..61de8bac 100644 --- a/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml +++ b/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml @@ -325,4 +325,19 @@ + + + "Local Folder" + Name + Name for this source. + + + + "" + Folder + The folder will be searched. + + + + diff --git a/randomwallpaper@iflow.space/sourceAdapter.js b/randomwallpaper@iflow.space/sourceAdapter.js deleted file mode 100755 index 28554205..00000000 --- a/randomwallpaper@iflow.space/sourceAdapter.js +++ /dev/null @@ -1,498 +0,0 @@ -const Gio = imports.gi.Gio; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); - -const RWG_SETTINGS_SCHEMA_UNSPLASH = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.unsplash'; -const RWG_SETTINGS_SCHEMA_WALLHAVEN = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.wallhaven'; -const RWG_SETTINGS_SCHEMA_REDDIT = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.reddit'; -const RWG_SETTINGS_SCHEMA_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; - -const SettingsModule = Self.imports.settings; -const HistoryModule = Self.imports.history; - -const LoggerModule = Self.imports.logger; -const JSONPath = Self.imports.jsonpath.jsonpath; - -/* - libSoup is accessed through the SoupBowl wrapper to support libSoup3 and libSoup2.4 simultaneously in the extension - runtime and in the preferences window. - */ -const SoupBowl = Self.imports.soupBowl; -const ByteArray = imports.byteArray; - -var BaseAdapter = class { - - constructor() { - this.logger = new LoggerModule.Logger('RWG3', 'BaseAdapter'); - } - - /** - * Retrieves a new url for an image and calls the given callback with an HistoryEntry as parameter. - * The history element will be null and the error will be set if an error occurred. - * - * @param callback(historyElement, error) - */ - requestRandomImage(callback) { - this._error("requestRandomImage not implemented", callback); - } - - fileName(uri) { - while (this._isURIEncoded(uri)) { - uri = decodeURIComponent(uri); - } - - let base = uri.substring(uri.lastIndexOf('/') + 1); - if (base.indexOf('?') >= 0) { - base = base.substr(0, base.indexOf('?')); - } - return base; - } - - /** - * copy file from uri to local wallpaper directory and calls the given callback with the name and the full filepath - * of the written file as parameter. - * @param uri - * @param callback(name, path, error) - */ - fetchFile(uri, callback) { - //extract the name from the url and - let date = new Date(); - let name = date.getTime() + '_' + this.fileName(uri); // timestamp ensures uniqueness - - let bowl = new SoupBowl.Bowl(); - - let file = Gio.file_new_for_path(this.wallpaperlocation + String(name)); - let fstream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); - - // start the download - let request = bowl.Soup.Message.new('GET', uri); - - bowl.send_and_receive(request, (response_data_bytes) => { - if (!response_data_bytes) { - fstream.close(null); - - if (callback) { - callback(null, null, 'Not a valid response'); - } - - return; - } - - try { - fstream.write(response_data_bytes, null); - - fstream.close(null); - - // call callback with the name and the full filepath of the written file as parameter - if (callback) { - callback(name, file.get_path()); - } - } catch (e) { - if (callback) { - callback(null, null, e); - } - } - }); - } - - _isURIEncoded(uri) { - uri = uri || ''; - - try { - return uri !== decodeURIComponent(uri); - } catch (err) { - this.logger.error(err); - return false; - } - } - - _error(err, callback) { - let error = { "error": err }; - this.logger.error(JSON.stringify(error)); - - if (callback) { - callback(null, error); - } - } - -}; - -var UnsplashAdapter = class extends BaseAdapter { - constructor(id) { - super(); - - this.sourceName = 'Unsplash'; - this.sourceUrl = 'https://source.unsplash.com'; - - // query options - this.options = { - 'query': '', - 'w': 1920, - 'h': 1080, - 'featured': false, - 'constraintType': '', - 'constraintValue': '', - }; - - if (id === null) { - id = -1; - } - - let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/unsplash/${id}/`; - this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_UNSPLASH, path); - this.bowl = new SoupBowl.Bowl(); - } - - requestRandomImage(callback) { - this._readOptionsFromSettings(); - let optionsString = this._generateOptionsString(); - - let identifier = this._settings.get("name", "string"); - if (identifier === null || identifier === "") { - identifier = this.sourceName; - } - - let url = `https://source.unsplash.com${optionsString}`; - url = encodeURI(url); - - this.logger.info(`Unsplash request to: ${url}`); - let message = this.bowl.Soup.Message.new('GET', url); - if (message === null) { - this._error("Could not create request.", callback); - return; - } - - // unsplash redirects to actual file; we only want the file location - message.set_flags(this.bowl.Soup.MessageFlags.NO_REDIRECT); - - this.bowl.send_and_receive(message, (_null_expected) => { - let imageLinkUrl; - - // expecting redirect - if (message.status_code !== 302) { - this._error("Unexpected response status code (expected 302)", callback); - } - - imageLinkUrl = message.response_headers.get_one('Location'); - - let historyEntry = new HistoryModule.HistoryEntry(null, identifier, imageLinkUrl); - historyEntry.source.sourceUrl = this.sourceUrl; - historyEntry.source.imageLinkUrl = imageLinkUrl; - callback(historyEntry); - }); - } - - _generateOptionsString() { - let options = this.options; - let optionsString = ""; - - switch (options.constraintType) { - case 'user': - optionsString = `/user/${options.constraintValue}/`; - break; - case 'likes': - optionsString = `/user/${options.constraintValue}/likes/`; - break; - case 'collection': - optionsString = `/collection/${options.constraintValue}/`; - break; - default: - if (options.featured) { - optionsString = `/featured/`; - } else { - optionsString = `/random/`; - } - } - - if (options.w && options.h) { - optionsString += `${options.w}x${options.h}`; - } - - if (options.query) { - let q = options.query.replace(/\W/, ','); - optionsString += `?${q}`; - } - - return optionsString; - } - - _readOptionsFromSettings() { - this.options.w = this._settings.get('image-width', 'int'); - this.options.h = this._settings.get('image-height', 'int'); - - this.options.constraintType = this._settings.get('constraint-type', 'string'); - this.options.constraintValue = this._settings.get('constraint-value', 'string'); - - const keywords = this._settings.get('keyword', 'string').split(","); - if (keywords.length > 0) { - const randomKeyword = keywords[Math.floor(Math.random() * keywords.length)]; - this.options.query = randomKeyword.trim(); - } - - this.options.featured = this._settings.get('featured-only', 'boolean'); - } -}; - -var WallhavenAdapter = class extends BaseAdapter { - constructor(id) { - super(); - - this.options = { - 'q': '', - 'apikey': '', - 'purity': '110', // SFW, sketchy - 'sorting': 'random', - 'categories': '111', // General, Anime, People - 'resolutions': ['1920x1200', '2560x1440'] - }; - - let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/wallhaven/${id}/`; - this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_WALLHAVEN, path); - this.bowl = new SoupBowl.Bowl(); - } - - requestRandomImage(callback) { - this._readOptionsFromSettings(); - let optionsString = this._generateOptionsString(); - - let identifier = this._settings.get("name", "string"); - if (identifier === null || identifier === "") { - identifier = 'Wallhaven'; - } - - let url = 'https://wallhaven.cc/api/v1/search?' + encodeURI(optionsString); - let message = this.bowl.Soup.Message.new('GET', url); - if (message === null) { - this._error("Could not create request.", callback); - return; - } - - this.bowl.send_and_receive(message, (response_body_bytes) => { - const response_body = ByteArray.toString(response_body_bytes); - - let response = JSON.parse(response_body).data; - - if (!response || response.length === 0) { - this._error("Failed to request image.", callback); - return; - } - - // get a random entry from the array - let entry = response[Math.floor(Math.random() * response.length)]; - let downloadURL = entry.path; - let siteURL = entry.url; - - let apiKey = this.options["apikey"]; - if (apiKey) { - downloadURL += "?apikey=" + apiKey; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(null, identifier, downloadURL); - historyEntry.source.sourceUrl = 'https://wallhaven.cc/'; - historyEntry.source.imageLinkUrl = siteURL; - callback(historyEntry); - } - }); - } - - _generateOptionsString() { - let options = this.options; - let optionsString = ""; - - for (let key in options) { - if (options.hasOwnProperty(key)) { - if (Array.isArray(options[key])) { - optionsString += key + "=" + options[key].join() + "&"; - } else { - if (options[key]) { - optionsString += key + "=" + options[key] + "&"; - } - } - } - } - - return optionsString; - } - - _readOptionsFromSettings() { - const keywords = this._settings.get('keyword', 'string').split(","); - if (keywords.length > 0) { - const randomKeyword = keywords[Math.floor(Math.random() * keywords.length)]; - this.options.q = randomKeyword.trim(); - } - this.options.apikey = this._settings.get('api-key', 'string'); - - this.options.resolutions = this._settings.get('resolutions', 'string').split(','); - this.options.resolutions = this.options.resolutions.map((elem) => { - return elem.trim(); - }); - - let categories = []; - categories.push(+this._settings.get('category-general', 'boolean')); // + is implicit conversion to int - categories.push(+this._settings.get('category-anime', 'boolean')); - categories.push(+this._settings.get('category-people', 'boolean')); - this.options.categories = categories.join(''); - - let purity = []; - purity.push(+this._settings.get('allow-sfw', 'boolean')); - purity.push(+this._settings.get('allow-sketchy', 'boolean')); - purity.push(+this._settings.get('allow-nsfw', 'boolean')); - this.options.purity = purity.join(''); - } -}; - -var RedditAdapter = class extends BaseAdapter { - constructor(id) { - super(); - - let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/reddit/${id}/`; - this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_REDDIT, path); - this.bowl = new SoupBowl.Bowl(); - } - - _ampDecode(string) { - return string.replace(/\&/g, '&'); - } - - requestRandomImage(callback) { - const subreddits = this._settings.get('subreddits', 'string').split(',').map(s => s.trim()).join('+'); - const require_sfw = this._settings.get('allow-sfw', 'boolean'); - let identifier = this._settings.get("name", "string"); - if (identifier === null || identifier === "") { - identifier = 'Reddit'; - } - - const url = encodeURI('https://www.reddit.com/r/' + subreddits + '.json'); - let message = this.bowl.Soup.Message.new('GET', url); - if (message === null) { - this._error("Could not create request.", callback); - return; - } - - this.bowl.send_and_receive(message, (response_body_bytes) => { - try { - const response_body = JSON.parse(ByteArray.toString(response_body_bytes)); - - const submissions = response_body.data.children.filter(child => { - if (child.data.post_hint !== 'image') return false; - if (require_sfw) return child.data.over_18 === false; - return true; - }); - if (submissions.length === 0) { - this._error("No suitable submissions found!", callback); - return; - } - const random = Math.floor(Math.random() * submissions.length); - const submission = submissions[random].data; - const imageDownloadUrl = this._ampDecode(submission.preview.images[0].source.url); - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(null, identifier, imageDownloadUrl); - historyEntry.source.sourceUrl = 'https://www.reddit.com/' + submission.subreddit_name_prefixed; - historyEntry.source.imageLinkUrl = 'https://www.reddit.com/' + submission.permalink; - callback(historyEntry); - } - } catch (e) { - this._error("Could not create request. (" + e + ")", callback); - } - }); - } - -}; - -var GenericJsonAdapter = class extends BaseAdapter { - constructor(id) { - super(); - this._jsonPathParser = new JSONPath.JSONPathParser(); - let path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/genericJSON/${id}/`; - this._settings = new SettingsModule.Settings(RWG_SETTINGS_SCHEMA_GENERIC_JSON, path); - this.bowl = new SoupBowl.Bowl(); - } - - requestRandomImage(callback) { - let url = this._settings.get("request-url", "string"); - url = encodeURI(url); - let message = this.bowl.Soup.Message.new('GET', url); - if (message === null) { - this._error("Could not create request.", callback); - return; - } - - this.bowl.send_and_receive(message, (response_body_bytes) => { - try { - const response_body = JSON.parse(ByteArray.toString(response_body_bytes)); - - let imageJSONPath = this._settings.get("image-path", "string"); - let postJSONPath = this._settings.get("post-path", "string"); - let domainUrl = this._settings.get("domain", "string"); - let authorNameJSONPath = this._settings.get("author-name-path", "string"); - let authorUrlJSONPath = this._settings.get("author-url-path", "string"); - - let identifier = this._settings.get("name", "string"); - if (identifier === null || identifier === "") { - identifier = 'Generic JSON Source'; - } - - let rObject = this._jsonPathParser.access(response_body, imageJSONPath); - let imageDownloadUrl = this._settings.get("image-prefix", "string") + rObject.Object; - - // '@random' would yield different results so lets make sure the values stay - // the same as long as the path is identical - let samePath = imageJSONPath.substring(0, this.findFirstDifference(imageJSONPath, postJSONPath)); - - // count occurrences of '@random' to slice the array later - // https://stackoverflow.com/a/4009768 - let occurrences = (samePath.match(/@random/g) || []).length; - let slicedRandomElements = rObject.RandomElements.slice(0, occurrences); - - let postUrl = this._jsonPathParser.access(response_body, postJSONPath, slicedRandomElements, false).Object; - postUrl = this._settings.get("post-prefix", "string") + postUrl; - if (typeof postUrl !== 'string' || !postUrl instanceof String) { - postUrl = null; - } - - let authorName = this._jsonPathParser.access(response_body, authorNameJSONPath, slicedRandomElements, false).Object; - if (typeof authorName !== 'string' || !authorName instanceof String) { - authorName = null; - } - - let authorUrl = this._jsonPathParser.access(response_body, authorUrlJSONPath, slicedRandomElements, false).Object; - authorUrl = this._settings.get("author-url-prefix", "string") + authorUrl; - if (typeof authorUrl !== 'string' || !authorUrl instanceof String) { - authorUrl = null; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(authorName, identifier, imageDownloadUrl); - - if (authorUrl !== null && authorUrl !== "") { - historyEntry.source.authorUrl = authorUrl; - } - - if (postUrl !== null && postUrl !== "") { - historyEntry.source.imageLinkUrl = postUrl; - } - - if (domainUrl !== null && domainUrl !== "") { - historyEntry.source.sourceUrl = domainUrl; - } - - callback(historyEntry); - } - } catch (e) { - this._error("Unexpected response. (" + e + ")", callback); - } - }); - - } - - // https://stackoverflow.com/a/32859917 - findFirstDifference(jsonPath1, jsonPath2) { - let i = 0; - if (jsonPath1 === jsonPath2) return -1; - while (jsonPath1[i] === jsonPath2[i]) i++; - return i; - } - -}; diff --git a/randomwallpaper@iflow.space/ui/generic_json.blp b/randomwallpaper@iflow.space/ui/genericJson.blp similarity index 100% rename from randomwallpaper@iflow.space/ui/generic_json.blp rename to randomwallpaper@iflow.space/ui/genericJson.blp diff --git a/randomwallpaper@iflow.space/ui/genericJson.js b/randomwallpaper@iflow.space/ui/genericJson.js new file mode 100644 index 00000000..b104e15d --- /dev/null +++ b/randomwallpaper@iflow.space/ui/genericJson.js @@ -0,0 +1,74 @@ +const Adw = imports.gi.Adw; +const ExtensionUtils = imports.misc.extensionUtils; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Self = ExtensionUtils.getCurrentExtension(); +const Convenience = Self.imports.convenience; + +const RWG_SETTINGS_SCHEMA_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; + +var GenericJsonSettingsGroup = GObject.registerClass({ + GTypeName: 'GenericJsonSettingsGroup', + Template: GLib.filename_to_uri(Self.path + '/ui/genericJson.ui', null), + InternalChildren: [ + 'author_name_path', + 'author_url_path', + 'author_url_prefix', + 'domain', + 'image_path', + 'image_prefix', + 'post_path', + 'post_prefix', + 'request_url' + ] +}, class GenericJsonSettingsGroup extends Adw.PreferencesGroup { + constructor(parent_row, params = {}) { + super(params); + + const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/genericJSON/${parent_row.id}/`; + this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_GENERIC_JSON, path); + + this._settings.bind('name', + parent_row.source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('domain', + this._domain, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('request-url', + this._request_url, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-path', + this._image_path, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-prefix', + this._image_prefix, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('post-path', + this._post_path, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('post-prefix', + this._post_prefix, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('author-name-path', + this._author_name_path, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('author-url-path', + this._author_url_path, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('author-url-prefix', + this._author_url_prefix, + 'text', + Gio.SettingsBindFlags.DEFAULT); + } +}); diff --git a/randomwallpaper@iflow.space/ui/generic_json.js b/randomwallpaper@iflow.space/ui/generic_json.js deleted file mode 100644 index 87303376..00000000 --- a/randomwallpaper@iflow.space/ui/generic_json.js +++ /dev/null @@ -1,74 +0,0 @@ -const Adw = imports.gi.Adw; -const ExtensionUtils = imports.misc.extensionUtils; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; - -const Self = ExtensionUtils.getCurrentExtension(); -const Convenience = Self.imports.convenience; - -const RWG_SETTINGS_SCHEMA_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; - -var GenericJsonSettingsGroup = GObject.registerClass({ - GTypeName: 'GenericJsonSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/generic_json.ui', null), - InternalChildren: [ - 'author_name_path', - 'author_url_path', - 'author_url_prefix', - 'domain', - 'image_path', - 'image_prefix', - 'post_path', - 'post_prefix', - 'request_url' - ] -}, class GenericJsonSettingsGroup extends Adw.PreferencesGroup { - constructor(parent_row, params = {}) { - super(params); - - const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/genericJSON/${parent_row.id}/`; - this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_GENERIC_JSON, path); - - this._settings.bind('name', - parent_row.source_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('domain', - this._domain, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('request-url', - this._request_url, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-path', - this._image_path, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-prefix', - this._image_prefix, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('post-path', - this._post_path, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('post-prefix', - this._post_prefix, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('author-name-path', - this._author_name_path, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('author-url-path', - this._author_url_path, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('author-url-prefix', - this._author_url_prefix, - 'text', - Gio.SettingsBindFlags.DEFAULT); - } -}); diff --git a/randomwallpaper@iflow.space/ui/localFolder.blp b/randomwallpaper@iflow.space/ui/localFolder.blp new file mode 100644 index 00000000..5e3ef6eb --- /dev/null +++ b/randomwallpaper@iflow.space/ui/localFolder.blp @@ -0,0 +1,18 @@ +using Gtk 4.0; +using Adw 1; + +template LocalFolderSettingsGroup : Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow folder_row { + title: "Folder"; + + Button folder { + valign: center; + + Adw.ButtonContent { + icon-name: "document-open-symbolic"; + } + } + } +} diff --git a/randomwallpaper@iflow.space/ui/localFolder.js b/randomwallpaper@iflow.space/ui/localFolder.js new file mode 100644 index 00000000..b92e3599 --- /dev/null +++ b/randomwallpaper@iflow.space/ui/localFolder.js @@ -0,0 +1,63 @@ +const Adw = imports.gi.Adw; +const ExtensionUtils = imports.misc.extensionUtils; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; + +const Self = ExtensionUtils.getCurrentExtension(); +const Convenience = Self.imports.convenience; + +const RWG_SETTINGS_SCHEMA_LOCAL_FOLDER = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.localFolder'; + +var LocalFolderSettingsGroup = GObject.registerClass({ + GTypeName: 'LocalFolderSettingsGroup', + Template: GLib.filename_to_uri(Self.path + '/ui/localFolder.ui', null), + InternalChildren: [ + 'folder', + 'folder_row' + ] +}, class LocalFolderSettingsGroup extends Adw.PreferencesGroup { + _saveDialog = null; + + constructor(parent_row, params = {}) { + super(params); + + const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/localFolder/${parent_row.id}/`; + this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_LOCAL_FOLDER, path); + + this._settings.bind('name', + parent_row.source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('folder', + this._folder_row, + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._folder.connect('clicked', () => { + // For GTK 4.10+ + // Gtk.FileDialog(); + + // https://stackoverflow.com/a/54487948 + this._saveDialog = new Gtk.FileChooserNative({ + title: 'Choose a Wallpaper Folder', + action: Gtk.FileChooserAction.SELECT_FOLDER, + accept_label: 'Open', + cancel_label: 'Cancel', + transient_for: this.get_root(), + modal: true, + }); + + this._saveDialog.connect('response', (dialog, response_id) => { + if (response_id === Gtk.ResponseType.ACCEPT) { + this._folder_row.text = this._saveDialog.get_file().get_path(); + } + this._saveDialog.destroy(); + }); + + this._saveDialog.show(); + }); + } +}); diff --git a/randomwallpaper@iflow.space/ui/page_general.blp b/randomwallpaper@iflow.space/ui/pageGeneral.blp similarity index 100% rename from randomwallpaper@iflow.space/ui/page_general.blp rename to randomwallpaper@iflow.space/ui/pageGeneral.blp diff --git a/randomwallpaper@iflow.space/ui/page_sources.blp b/randomwallpaper@iflow.space/ui/pageSources.blp similarity index 100% rename from randomwallpaper@iflow.space/ui/page_sources.blp rename to randomwallpaper@iflow.space/ui/pageSources.blp diff --git a/randomwallpaper@iflow.space/ui/reddit.js b/randomwallpaper@iflow.space/ui/reddit.js index 4a6e72fe..b513ef35 100644 --- a/randomwallpaper@iflow.space/ui/reddit.js +++ b/randomwallpaper@iflow.space/ui/reddit.js @@ -10,30 +10,30 @@ const Convenience = Self.imports.convenience; const RWG_SETTINGS_SCHEMA_REDDIT = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.reddit'; var RedditSettingsGroup = GObject.registerClass({ - GTypeName: 'RedditSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/reddit.ui', null), - InternalChildren: [ - 'allow_sfw', - 'subreddits' - ] + GTypeName: 'RedditSettingsGroup', + Template: GLib.filename_to_uri(Self.path + '/ui/reddit.ui', null), + InternalChildren: [ + 'allow_sfw', + 'subreddits' + ] }, class RedditSettingsGroup extends Adw.PreferencesGroup { - constructor(parent_row, params = {}) { - super(params); + constructor(parent_row, params = {}) { + super(params); - const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/reddit/${parent_row.id}/`; - this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_REDDIT, path); + const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/reddit/${parent_row.id}/`; + this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_REDDIT, path); - this._settings.bind('name', - parent_row.source_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('allow-sfw', - this._allow_sfw, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('subreddits', - this._subreddits, - 'text', - Gio.SettingsBindFlags.DEFAULT); - } + this._settings.bind('name', + parent_row.source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('allow-sfw', + this._allow_sfw, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('subreddits', + this._subreddits, + 'text', + Gio.SettingsBindFlags.DEFAULT); + } }); diff --git a/randomwallpaper@iflow.space/ui/source_row.blp b/randomwallpaper@iflow.space/ui/sourceRow.blp similarity index 97% rename from randomwallpaper@iflow.space/ui/source_row.blp rename to randomwallpaper@iflow.space/ui/sourceRow.blp index a939f10f..b57359ba 100644 --- a/randomwallpaper@iflow.space/ui/source_row.blp +++ b/randomwallpaper@iflow.space/ui/sourceRow.blp @@ -29,6 +29,7 @@ template SourceRow : Adw.ExpanderRow { "Wallhaven", "Reddit", _("Generic JSON"), + _("Local Folder"), ] }; } diff --git a/randomwallpaper@iflow.space/ui/sourceRow.js b/randomwallpaper@iflow.space/ui/sourceRow.js new file mode 100644 index 00000000..f90ae465 --- /dev/null +++ b/randomwallpaper@iflow.space/ui/sourceRow.js @@ -0,0 +1,87 @@ +const Adw = imports.gi.Adw; +const ExtensionUtils = imports.misc.extensionUtils; +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; + +const Self = ExtensionUtils.getCurrentExtension(); +const Convenience = Self.imports.convenience; + +const Unsplash = Self.imports.ui.unsplash; +const Wallhaven = Self.imports.ui.wallhaven; +const Reddit = Self.imports.ui.reddit; +const GenericJson = Self.imports.ui.genericJson; +const LocalFolder = Self.imports.ui.localFolder; + +// https://gitlab.gnome.org/GNOME/gjs/-/blob/master/examples/gtk4-template.js +var SourceRow = GObject.registerClass({ + GTypeName: 'SourceRow', + Template: GLib.filename_to_uri(Self.path + '/ui/sourceRow.ui', null), + Children: [ + 'button_delete', + 'combo', + 'source_name' + ], + InternalChildren: [ + 'settings_container' + ] +}, class SourceRow extends Adw.ExpanderRow { + constructor(configObject = null, params = {}) { + super(params); + + if (configObject === null) { + // New row + this.id = Date.now(); + this.combo.set_selected(0); + this.set_enable_expansion(true); + this._settings_container.set_child(new Unsplash.UnsplashSettingsGroup(this)); + } else { + // Row from config + this.id = configObject.id; + this.combo.set_selected(configObject.type); + this.set_enable_expansion(configObject.enabled); + + this._fillRow(this.combo.selected); + } + + this.combo.connect('notify::selected', comboRow => { + this._clearConfig() + this._fillRow(comboRow.selected); + }); + } + + _clearConfig() { + // TODO: clear remainder? + // this._settings_container.get_child().unbind(this); + Gio.Settings.unbind(this.source_name, 'text'); + } + + _fillRow(type) { + let targetWidget = null; + switch (type) { + case 0: // unsplash + targetWidget = new Unsplash.UnsplashSettingsGroup(this); + break; + case 1: // wallhaven + targetWidget = new Wallhaven.WallhavenSettingsGroup(this); + break; + case 2: // reddit + targetWidget = new Reddit.RedditSettingsGroup(this); + break; + case 3: // generic JSON + targetWidget = new GenericJson.GenericJsonSettingsGroup(this); + break; + case 4: // Local Folder + targetWidget = new LocalFolder.LocalFolderSettingsGroup(this); + break; + default: + targetWidget = null; + this.logger.error("The selected source has no corresponding widget!") + break; + } + + if (targetWidget !== null) { + this._settings_container.set_child(targetWidget); + } + } +}); diff --git a/randomwallpaper@iflow.space/ui/source_row.js b/randomwallpaper@iflow.space/ui/source_row.js deleted file mode 100644 index 723a87a2..00000000 --- a/randomwallpaper@iflow.space/ui/source_row.js +++ /dev/null @@ -1,83 +0,0 @@ -const Adw = imports.gi.Adw; -const ExtensionUtils = imports.misc.extensionUtils; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; - -const Self = ExtensionUtils.getCurrentExtension(); -const Convenience = Self.imports.convenience; - -const Unsplash = Self.imports.ui.unsplash; -const Wallhaven = Self.imports.ui.wallhaven; -const Reddit = Self.imports.ui.reddit; -const GenericJson = Self.imports.ui.generic_json; - -// https://gitlab.gnome.org/GNOME/gjs/-/blob/master/examples/gtk4-template.js -var SourceRow = GObject.registerClass({ - GTypeName: 'SourceRow', - Template: GLib.filename_to_uri(Self.path + '/ui/source_row.ui', null), - Children: [ - 'button_delete', - 'combo', - 'source_name' - ], - InternalChildren: [ - 'settings_container' - ] -}, class SourceRow extends Adw.ExpanderRow { - constructor(configObject = null, params = {}) { - super(params); - - if (configObject === null) { - // New row - this.id = Date.now(); - this.combo.set_selected(0); - this.set_enable_expansion(true); - this._settings_container.set_child(new Unsplash.UnsplashSettingsGroup(this)); - } else { - // Row from config - this.id = configObject.id; - this.combo.set_selected(configObject.type); - this.set_enable_expansion(configObject.enabled); - - this._fill_row(this.combo.selected); - } - - this.combo.connect('notify::selected', comboRow => { - this._clear_config() - this._fill_row(comboRow.selected); - }); - } - - _clear_config() { - // TODO: clear remainder? - // this._settings_container.get_child().unbind(this); - Gio.Settings.unbind(this.source_name, 'text'); - } - - _fill_row(type) { - let targetWidget = null; - switch (type) { - case 0: // unsplash - targetWidget = new Unsplash.UnsplashSettingsGroup(this); - break; - case 1: // wallhaven - targetWidget = new Wallhaven.WallhavenSettingsGroup(this); - break; - case 2: // reddit - targetWidget = new Reddit.RedditSettingsGroup(this); - break; - case 3: // generic JSON - targetWidget = new GenericJson.GenericJsonSettingsGroup(this); - break; - default: - targetWidget = null; - this.logger.error("The selected source has no corresponding widget!") - break; - } - - if (targetWidget !== null) { - this._settings_container.set_child(targetWidget); - } - } -}); diff --git a/randomwallpaper@iflow.space/ui/unsplash.js b/randomwallpaper@iflow.space/ui/unsplash.js index 30bfdb4e..fe27e15a 100644 --- a/randomwallpaper@iflow.space/ui/unsplash.js +++ b/randomwallpaper@iflow.space/ui/unsplash.js @@ -10,67 +10,67 @@ const Convenience = Self.imports.convenience; const RWG_SETTINGS_SCHEMA_UNSPLASH = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.unsplash'; var UnsplashSettingsGroup = GObject.registerClass({ - GTypeName: 'UnsplashSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/unsplash.ui', null), - InternalChildren: [ - 'constraint_type', - 'constraint_value', - 'featured_only', - 'image_height', - 'image_width', - 'keyword' - ] + GTypeName: 'UnsplashSettingsGroup', + Template: GLib.filename_to_uri(Self.path + '/ui/unsplash.ui', null), + InternalChildren: [ + 'constraint_type', + 'constraint_value', + 'featured_only', + 'image_height', + 'image_width', + 'keyword' + ] }, class UnsplashSettingsGroup extends Adw.PreferencesGroup { - constructor(parent_row, params = {}) { - super(params); + constructor(parent_row, params = {}) { + super(params); - const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/unsplash/${parent_row.id}/`; - this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_UNSPLASH, path); + const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/unsplash/${parent_row.id}/`; + this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_UNSPLASH, path); - this._settings.bind('name', - parent_row.source_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('keyword', - this._keyword, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-width', - this._image_width, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-height', - this._image_height, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('featured-only', - this._featured_only, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('constraint-type', - this._constraint_type, - 'selected', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('constraint-value', - this._constraint_value, - 'text', - Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('name', + parent_row.source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('keyword', + this._keyword, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-width', + this._image_width, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-height', + this._image_height, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('featured-only', + this._featured_only, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('constraint-type', + this._constraint_type, + 'selected', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('constraint-value', + this._constraint_value, + 'text', + Gio.SettingsBindFlags.DEFAULT); - this._unsplashUnconstrained(this._constraint_type, true, this._featured_only); - this._unsplashUnconstrained(this._constraint_type, false, this._constraint_value); - this._constraint_type.connect('notify::selected', (comboRow) => { - this._unsplashUnconstrained(comboRow, true, this._featured_only); - this._unsplashUnconstrained(comboRow, false, this._constraint_value); + this._unsplashUnconstrained(this._constraint_type, true, this._featured_only); + this._unsplashUnconstrained(this._constraint_type, false, this._constraint_value); + this._constraint_type.connect('notify::selected', (comboRow) => { + this._unsplashUnconstrained(comboRow, true, this._featured_only); + this._unsplashUnconstrained(comboRow, false, this._constraint_value); - this._featured_only.set_active(false); - }); - } + this._featured_only.set_active(false); + }); + } - _unsplashUnconstrained(comboRow, enable, targetElement) { - if (comboRow.selected === 0) { - targetElement.set_sensitive(enable); - } else { - targetElement.set_sensitive(!enable); - } - } + _unsplashUnconstrained(comboRow, enable, targetElement) { + if (comboRow.selected === 0) { + targetElement.set_sensitive(enable); + } else { + targetElement.set_sensitive(!enable); + } + } }); diff --git a/randomwallpaper@iflow.space/ui/wallhaven.js b/randomwallpaper@iflow.space/ui/wallhaven.js index 657b06ab..ea3ace9f 100644 --- a/randomwallpaper@iflow.space/ui/wallhaven.js +++ b/randomwallpaper@iflow.space/ui/wallhaven.js @@ -10,65 +10,65 @@ const Convenience = Self.imports.convenience; const RWG_SETTINGS_SCHEMA_WALLHAVEN = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.wallhaven'; var WallhavenSettingsGroup = GObject.registerClass({ - GTypeName: 'WallhavenSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/wallhaven.ui', null), - InternalChildren: [ - 'allow_sfw', - 'allow_sketchy', - 'allow_nsfw', - 'api_key', - 'category_anime', - 'category_general', - 'category_people', - 'keyword', - 'resolutions' - ] + GTypeName: 'WallhavenSettingsGroup', + Template: GLib.filename_to_uri(Self.path + '/ui/wallhaven.ui', null), + InternalChildren: [ + 'allow_sfw', + 'allow_sketchy', + 'allow_nsfw', + 'api_key', + 'category_anime', + 'category_general', + 'category_people', + 'keyword', + 'resolutions' + ] }, class WallhavenSettingsGroup extends Adw.PreferencesGroup { - constructor(parent_row, params = {}) { - super(params); + constructor(parent_row, params = {}) { + super(params); - const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/wallhaven/${parent_row.id}/`; - this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_WALLHAVEN, path); + const path = `/org/gnome/shell/extensions/space-iflow-randomwallpaper/sources/wallhaven/${parent_row.id}/`; + this._settings = Convenience.getSettings(RWG_SETTINGS_SCHEMA_WALLHAVEN, path); - this._settings.bind('name', - parent_row.source_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('keyword', - this._keyword, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('api-key', - this._api_key, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('resolutions', - this._resolutions, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-general', - this._category_general, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-anime', - this._category_anime, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-people', - this._category_people, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('allow-sfw', - this._allow_sfw, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('allow-sketchy', - this._allow_sketchy, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('allow-nsfw', - this._allow_nsfw, - 'active', - Gio.SettingsBindFlags.DEFAULT); - } + this._settings.bind('name', + parent_row.source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('keyword', + this._keyword, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('api-key', + this._api_key, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('resolutions', + this._resolutions, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-general', + this._category_general, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-anime', + this._category_anime, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-people', + this._category_people, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('allow-sfw', + this._allow_sfw, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('allow-sketchy', + this._allow_sketchy, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('allow-nsfw', + this._allow_nsfw, + 'active', + Gio.SettingsBindFlags.DEFAULT); + } }); diff --git a/randomwallpaper@iflow.space/wallpaperController.js b/randomwallpaper@iflow.space/wallpaperController.js index 9b6b5ce8..fa31b666 100644 --- a/randomwallpaper@iflow.space/wallpaperController.js +++ b/randomwallpaper@iflow.space/wallpaperController.js @@ -6,12 +6,17 @@ const GLib = imports.gi.GLib; //self const Self = imports.misc.extensionUtils.getCurrentExtension(); -const SourceAdapter = Self.imports.sourceAdapter; +const HistoryModule = Self.imports.history; +const LoggerModule = Self.imports.logger; const Prefs = Self.imports.settings; const Timer = Self.imports.timer; -const HistoryModule = Self.imports.history; -const LoggerModule = Self.imports.logger; +// SourceAdapter +const GenericJsonAdapter = Self.imports.adapter.genericJson; +const LocalFolderAdapter = Self.imports.adapter.localFolder; +const RedditAdapter = Self.imports.adapter.reddit; +const UnsplashAdapter = Self.imports.adapter.unsplash; +const WallhavenAdapter = Self.imports.adapter.wallhaven; const RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.backend-connection'; @@ -136,26 +141,33 @@ var WallpaperController = class { */ _getRandomAdapter() { let imageSourceAdapter = null; - let source = this._getRandomSource(); - switch (source.type) { - case 0: - imageSourceAdapter = new SourceAdapter.UnsplashAdapter(source.id); - break; - case 1: - imageSourceAdapter = new SourceAdapter.WallhavenAdapter(source.id); - break; - case 2: - imageSourceAdapter = new SourceAdapter.RedditAdapter(source.id); - break; - case 3: - imageSourceAdapter = new SourceAdapter.GenericJsonAdapter(source.id); - break; - default: - imageSourceAdapter = new SourceAdapter.UnsplashAdapter(null); - // TODO: log error and abort, raise exception? - break; + try { + switch (source.type) { + case 0: + imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(source.id, this.wallpaperlocation); + break; + case 1: + imageSourceAdapter = new WallhavenAdapter.WallhavenAdapter(source.id, this.wallpaperlocation); + break; + case 2: + imageSourceAdapter = new RedditAdapter.RedditAdapter(source.id, this.wallpaperlocation); + break; + case 3: + imageSourceAdapter = new GenericJsonAdapter.GenericJsonAdapter(source.id, this.wallpaperlocation); + break; + case 4: + imageSourceAdapter = new LocalFolderAdapter.LocalFolderAdapter(source.id, this.wallpaperlocation); + break; + default: + imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(null, this.wallpaperlocation); + // TODO: log error and abort, raise exception? + break; + } + } catch (error) { + this.logger.warn("Had errors, fetching with default settings."); + imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(null, this.wallpaperlocation); } return imageSourceAdapter;